sessionmem 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +372 -365
- package/dist/adapters/capabilities/fallbackTools.js +33 -18
- package/dist/adapters/claudeMdInjector.js +164 -0
- package/dist/adapters/factory.js +68 -9
- package/dist/adapters/generic.js +221 -15
- 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 +54 -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/adapters/tools/ping.js +4 -1
- package/dist/cli/commands/config.js +10 -1
- package/dist/cli/commands/import.js +6 -1
- package/dist/cli/commands/install.js +63 -5
- package/dist/cli/commands/ping.js +42 -8
- package/dist/cli/commands/reEmbed.js +48 -0
- package/dist/cli/commands/run.js +18 -2
- package/dist/cli/commands/savings.js +91 -0
- 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 +37 -1
- package/dist/cli/context.js +14 -18
- package/dist/cli/index.js +30 -4
- package/dist/cli/output.js +11 -3
- package/dist/cli/projectId.js +69 -0
- package/dist/core/api/contracts.js +182 -45
- package/dist/core/api/errors.js +4 -7
- package/dist/core/api/memoryCoreService.js +409 -240
- package/dist/core/api/sessionLifecycleService.js +20 -2
- package/dist/core/config/policyConfig.js +53 -6
- package/dist/core/injection/formatStartupInjection.js +55 -10
- package/dist/core/injection/tokenBudget.js +8 -0
- package/dist/core/retrieve/importance.js +4 -3
- package/dist/core/retrieve/recencyBands.js +6 -10
- package/dist/core/retrieve/retrieveMemories.js +19 -4
- package/dist/core/retrieve/score.js +11 -1
- package/dist/core/schema/migrations/005_team_provenance.sql +14 -9
- package/dist/core/schema/migrations/006_access_pattern_boosting.sql +10 -0
- package/dist/core/schema/migrations/007_feedback_manual_delete.sql +23 -0
- package/dist/core/schema/migrations/008_fts5_search.sql +37 -0
- package/dist/core/schema/migrations/009_session_events_unique.sql +24 -0
- package/dist/core/schema/runMigrations.js +64 -2
- package/dist/core/storage/db.js +6 -0
- package/dist/core/storage/memoryFeedbackRepo.js +14 -4
- package/dist/core/storage/memoryRepo.js +292 -121
- package/dist/core/storage/memorySearchRepo.js +125 -13
- package/dist/core/storage/sessionEventsRepo.js +33 -10
- package/dist/core/storage/summarizationFailuresRepo.js +36 -26
- package/dist/core/storage/tokenSavingsRepo.js +20 -0
- package/dist/core/summarize/cloudSummarizer.js +34 -5
- package/dist/core/summarize/localSummarizer.js +1 -10
- package/dist/core/summarize/redaction.js +45 -8
- package/package.json +50 -48
|
@@ -2,8 +2,31 @@ import { existsSync, rmSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { AdapterFactory } from "../../adapters/factory.js";
|
|
5
|
+
import { removeClaudeMdBlock } from "../../adapters/claudeMdInjector.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the adapter to uninstall from: an explicit `--adapter` flag wins,
|
|
8
|
+
* then the SESSIONMEM_ADAPTER env override, else auto-detection. Throws (via
|
|
9
|
+
* AdapterFactory.forName) on an unknown explicit name.
|
|
10
|
+
*/
|
|
11
|
+
function resolveUninstallAdapter(options) {
|
|
12
|
+
const explicit = options.adapter && options.adapter.trim() !== ""
|
|
13
|
+
? options.adapter.trim()
|
|
14
|
+
: process.env.SESSIONMEM_ADAPTER && process.env.SESSIONMEM_ADAPTER.trim() !== ""
|
|
15
|
+
? process.env.SESSIONMEM_ADAPTER.trim()
|
|
16
|
+
: undefined;
|
|
17
|
+
return explicit
|
|
18
|
+
? AdapterFactory.forName(explicit)
|
|
19
|
+
: AdapterFactory.detectAdapter();
|
|
20
|
+
}
|
|
5
21
|
export async function uninstallCommand(options = {}) {
|
|
6
|
-
|
|
22
|
+
let adapter;
|
|
23
|
+
try {
|
|
24
|
+
adapter = resolveUninstallAdapter(options);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
7
30
|
if (!adapter.uninstall) {
|
|
8
31
|
console.error(`${adapter.name} does not support automated uninstall. Remove sessionmem from your MCP config manually.`);
|
|
9
32
|
process.exit(1);
|
|
@@ -14,6 +37,19 @@ export async function uninstallCommand(options = {}) {
|
|
|
14
37
|
process.exit(1);
|
|
15
38
|
}
|
|
16
39
|
console.log(`✓ ${adapter.name} config removed`);
|
|
40
|
+
// Guidance cleanup — non-fatal. Remove the sessionmem block from every file
|
|
41
|
+
// the adapter injected it into at install time. Falls back to the global
|
|
42
|
+
// Claude Code memory file when the adapter declares no targets.
|
|
43
|
+
const guidanceTargets = adapter.guidanceTargets?.() ?? [join(homedir(), ".claude", "CLAUDE.md")];
|
|
44
|
+
for (const target of guidanceTargets) {
|
|
45
|
+
try {
|
|
46
|
+
removeClaudeMdBlock(target);
|
|
47
|
+
console.log(`✓ Agent guidance removed (${target})`);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
console.error(`✗ Agent guidance cleanup failed (non-fatal): ${target}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
17
53
|
// Resolve dbPath: use injected override (for tests) or the default location
|
|
18
54
|
const dbPath = options.dbPath ?? join(homedir(), ".sessionmem", "memories.db");
|
|
19
55
|
if (options.purge) {
|
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,24 @@ function safeUserInfoName() {
|
|
|
28
29
|
return "";
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
return join(homedir(), p.slice(2));
|
|
42
|
+
}
|
|
43
|
+
return p;
|
|
48
44
|
}
|
|
49
45
|
function defaultDbPath(dir) {
|
|
50
46
|
// Env override seam (see deriveProjectId). Defaults to ~/.sessionmem/memories.db.
|
|
51
47
|
const envDbPath = process.env.SESSIONMEM_DB_PATH;
|
|
52
48
|
if (envDbPath && envDbPath.trim() !== "")
|
|
53
|
-
return envDbPath;
|
|
49
|
+
return expandTilde(envDbPath);
|
|
54
50
|
// `dir` is the fixed ~/.sessionmem dir computed above, not user input.
|
|
55
51
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
56
52
|
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";
|
|
@@ -12,11 +14,13 @@ import { forgetCommand } from "./commands/forget.js";
|
|
|
12
14
|
import { exportCommand } from "./commands/export.js";
|
|
13
15
|
import { importCommand } from "./commands/import.js";
|
|
14
16
|
import { statsCommand } from "./commands/stats.js";
|
|
17
|
+
import { savingsCommand } from "./commands/savings.js";
|
|
15
18
|
import { redactScanCommand } from "./commands/redactScan.js";
|
|
16
19
|
import { retentionPruneCommand } from "./commands/retention.js";
|
|
17
20
|
import { configGetCommand, configSetCommand } from "./commands/config.js";
|
|
18
21
|
import { teamEnableCommand, teamDisableCommand, teamStatusCommand, } from "./commands/team.js";
|
|
19
22
|
import { syncCommand } from "./commands/sync.js";
|
|
23
|
+
import { reEmbedCommand } from "./commands/reEmbed.js";
|
|
20
24
|
// Source the version from package.json (single source of truth) so `--version`
|
|
21
25
|
// never drifts from the published manifest. createRequire + resolveJsonModule
|
|
22
26
|
// reads the manifest relative to this module; from dist/cli/index.js the
|
|
@@ -28,19 +32,29 @@ program
|
|
|
28
32
|
.command("run")
|
|
29
33
|
.description("Start the sessionmem MCP server")
|
|
30
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());
|
|
31
43
|
program
|
|
32
44
|
.command("install")
|
|
33
45
|
.description("Install sessionmem into the current MCP host")
|
|
34
|
-
.
|
|
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));
|
|
35
48
|
program
|
|
36
49
|
.command("uninstall")
|
|
37
50
|
.description("Remove sessionmem from the current MCP host")
|
|
38
51
|
.option("--purge", "Also delete the local memories database")
|
|
39
|
-
.
|
|
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));
|
|
40
54
|
program
|
|
41
55
|
.command("ping")
|
|
42
|
-
.description("Check sessionmem
|
|
43
|
-
.action(pingCommand);
|
|
56
|
+
.description("Check sessionmem version and local database reachability")
|
|
57
|
+
.action(() => pingCommand());
|
|
44
58
|
// NOTE: commander always appends its own Command instance as the final
|
|
45
59
|
// argument to .action() callbacks. The search/list/show/forget/export/import/
|
|
46
60
|
// stats commands all declare a trailing `ctx?: CliContext` parameter (the
|
|
@@ -81,6 +95,11 @@ program
|
|
|
81
95
|
.command("stats")
|
|
82
96
|
.description("Show memory statistics for the current project")
|
|
83
97
|
.action(() => statsCommand());
|
|
98
|
+
program
|
|
99
|
+
.command("savings")
|
|
100
|
+
.description("Show token savings from sessionmem compression and injection")
|
|
101
|
+
.option("--json", "Output raw metrics as JSON")
|
|
102
|
+
.action((options) => savingsCommand(undefined, options));
|
|
84
103
|
// redact-scan — one-time scrub over existing memories. Scan is the
|
|
85
104
|
// non-destructive default; --apply redacts matching rows in place.
|
|
86
105
|
program
|
|
@@ -134,6 +153,13 @@ team
|
|
|
134
153
|
.command("status")
|
|
135
154
|
.description("Show team mode state and shared-path availability")
|
|
136
155
|
.action(() => teamStatusCommand());
|
|
156
|
+
// re-embed — bulk-update stale embeddings to the current version.
|
|
157
|
+
// reEmbedCommand declares a trailing `ctx?` test seam, so arrow-wrap to drop
|
|
158
|
+
// commander's trailing Command argument (NOTE above).
|
|
159
|
+
program
|
|
160
|
+
.command("re-embed")
|
|
161
|
+
.description("Re-embed all memories with stale embedding versions")
|
|
162
|
+
.action(() => reEmbedCommand());
|
|
137
163
|
// sync — push a local snapshot to the shared path and pull every
|
|
138
164
|
// teammate snapshot back. syncCommand declares a trailing `ctx?` test seam, so
|
|
139
165
|
// arrow-wrap to drop commander's trailing Command argument (NOTE above).
|
package/dist/cli/output.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
export function formatTable(rows) {
|
|
2
2
|
const ID_WIDTH = 36;
|
|
3
|
-
const IMP_WIDTH =
|
|
3
|
+
const IMP_WIDTH = 14;
|
|
4
|
+
const ACC_WIDTH = 8;
|
|
4
5
|
const DATE_WIDTH = 10;
|
|
5
|
-
const PREVIEW_WIDTH =
|
|
6
|
+
const PREVIEW_WIDTH = 50;
|
|
6
7
|
const header = "ID".padEnd(ID_WIDTH) +
|
|
7
8
|
" | " +
|
|
8
9
|
"importance".padEnd(IMP_WIDTH) +
|
|
9
10
|
" | " +
|
|
11
|
+
"accesses".padEnd(ACC_WIDTH) +
|
|
12
|
+
" | " +
|
|
10
13
|
"date".padEnd(DATE_WIDTH) +
|
|
11
14
|
" | " +
|
|
12
15
|
"preview";
|
|
13
16
|
const lines = rows.map((row) => {
|
|
14
17
|
const preview = row.content.replace(/\s+/g, " ").slice(0, PREVIEW_WIDTH);
|
|
15
18
|
const date = row.createdAt.slice(0, 10);
|
|
19
|
+
const imp = row.effectiveImportance !== row.importance
|
|
20
|
+
? `${row.importance}(${row.effectiveImportance})`
|
|
21
|
+
: String(row.importance);
|
|
16
22
|
return (row.id.padEnd(ID_WIDTH) +
|
|
17
23
|
" | " +
|
|
18
|
-
|
|
24
|
+
imp.padEnd(IMP_WIDTH) +
|
|
25
|
+
" | " +
|
|
26
|
+
String(row.accessCount).padEnd(ACC_WIDTH) +
|
|
19
27
|
" | " +
|
|
20
28
|
date.padEnd(DATE_WIDTH) +
|
|
21
29
|
" | " +
|
|
@@ -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),
|
|
@@ -11,27 +55,47 @@ export const memorySchema = z.object({
|
|
|
11
55
|
embedding: z.string().nullable(),
|
|
12
56
|
embeddingDim: z.number().int().nullable(),
|
|
13
57
|
embeddingVersion: z.string().nullable(),
|
|
58
|
+
accessCount: z.number().int().nonnegative(),
|
|
59
|
+
lastAccessed: z.string().nullable(),
|
|
60
|
+
effectiveImportance: z.number().int().min(1).max(10),
|
|
14
61
|
createdAt: z.string().min(1),
|
|
15
62
|
updatedAt: z.string().min(1),
|
|
16
63
|
});
|
|
17
64
|
export const ingestSessionEventSchema = z.object({
|
|
18
|
-
id: z.string().min(1),
|
|
65
|
+
id: z.string().min(1).max(200),
|
|
19
66
|
eventIndex: z.number().int().nonnegative(),
|
|
20
|
-
eventType: z.string().min(1),
|
|
21
|
-
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" }),
|
|
22
81
|
createdAt: z.string().min(1).optional(),
|
|
23
82
|
});
|
|
24
83
|
export const ingestSessionEventsRequestSchema = z.object({
|
|
25
84
|
projectId: z.string().min(1),
|
|
26
|
-
sessionId: z.string().min(1),
|
|
27
|
-
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),
|
|
28
87
|
});
|
|
29
88
|
export const summarizeSessionToMemoryRequestSchema = z.object({
|
|
30
|
-
memoryId: z.string().min(1),
|
|
89
|
+
memoryId: z.string().min(1).max(200),
|
|
31
90
|
projectId: z.string().min(1),
|
|
32
|
-
sessionId: z.string().min(1),
|
|
33
|
-
sourceAdapter: z
|
|
34
|
-
|
|
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),
|
|
35
99
|
importance: z.number().int().min(1).max(10),
|
|
36
100
|
});
|
|
37
101
|
export const factModeSchema = z.enum([
|
|
@@ -42,7 +106,7 @@ export const factModeSchema = z.enum([
|
|
|
42
106
|
export const handleSessionEndConfigSchema = z.object({
|
|
43
107
|
autoSummarize: z.boolean().default(true),
|
|
44
108
|
minimumEventThreshold: z.number().int().min(1).max(100).default(3),
|
|
45
|
-
summaryTokenCap: z.number().int().min(1).default(300),
|
|
109
|
+
summaryTokenCap: z.number().int().min(1).max(200000).default(300),
|
|
46
110
|
// No `.default()`: omission must be distinguishable from an explicit value so
|
|
47
111
|
// the service layer can fall back to the policy-config redactionEnabled
|
|
48
112
|
// setting (override > config.json > default precedence).
|
|
@@ -53,18 +117,28 @@ export const handleSessionEndConfigSchema = z.object({
|
|
|
53
117
|
});
|
|
54
118
|
export const handleSessionEndRequestSchema = z.object({
|
|
55
119
|
projectId: z.string().min(1),
|
|
56
|
-
sessionId: z.string().min(1),
|
|
57
|
-
sourceAdapter: z
|
|
58
|
-
|
|
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(),
|
|
59
128
|
config: handleSessionEndConfigSchema.default(() => handleSessionEndConfigSchema.parse({})),
|
|
60
129
|
});
|
|
61
130
|
export const storeMemoryRequestSchema = z.object({
|
|
62
|
-
memoryId: z.string().min(1),
|
|
131
|
+
memoryId: z.string().min(1).max(200),
|
|
63
132
|
projectId: z.string().min(1),
|
|
64
|
-
sessionId: z.string().min(1),
|
|
65
|
-
sourceAdapter: z
|
|
66
|
-
|
|
67
|
-
|
|
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),
|
|
68
142
|
importance: z.number().int().min(1).max(10),
|
|
69
143
|
// No `.default()`: omission must be distinguishable from an explicit value so
|
|
70
144
|
// the service layer can fall back to the policy-config redactionEnabled
|
|
@@ -73,38 +147,43 @@ export const storeMemoryRequestSchema = z.object({
|
|
|
73
147
|
});
|
|
74
148
|
export const retrieveMemoriesRequestSchema = z.object({
|
|
75
149
|
projectId: z.string().min(1),
|
|
76
|
-
query: z.string().min(1),
|
|
150
|
+
query: z.string().min(1).max(1000),
|
|
77
151
|
limit: z.number().int().min(1).max(100).default(20),
|
|
78
152
|
mode: z.enum(["auto", "on-demand"]).default("auto"),
|
|
79
153
|
depth: z.enum(["default", "deep"]).default("default"),
|
|
80
154
|
});
|
|
81
|
-
export const recordMemoryUsedRequestSchema = z.object({
|
|
82
|
-
projectId: z.string().min(1),
|
|
83
|
-
memoryId: z.string().min(1),
|
|
84
|
-
feedbackType: z.enum(["auto_use", "manual"]).default("auto_use"),
|
|
85
|
-
usedAt: z.string().min(1).optional(),
|
|
86
|
-
});
|
|
87
155
|
export const listMemoriesRequestSchema = z.object({
|
|
88
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(),
|
|
89
163
|
});
|
|
90
164
|
export const getMemoryRequestSchema = z.object({
|
|
91
165
|
projectId: z.string().min(1),
|
|
92
|
-
memoryId: z.string().min(1),
|
|
166
|
+
memoryId: z.string().min(1).max(200),
|
|
93
167
|
});
|
|
94
168
|
export const forgetMemoryRequestSchema = z.object({
|
|
95
169
|
projectId: z.string().min(1),
|
|
96
|
-
memoryId: z.string().min(1),
|
|
170
|
+
memoryId: z.string().min(1).max(200),
|
|
97
171
|
});
|
|
98
172
|
export const exportMemoriesRequestSchema = z.object({
|
|
99
173
|
projectId: z.string().min(1),
|
|
100
174
|
});
|
|
101
175
|
export const importMemoryRecordSchema = z.object({
|
|
102
|
-
id: z.string().min(1),
|
|
176
|
+
id: z.string().min(1).max(200),
|
|
103
177
|
projectId: z.string().min(1),
|
|
104
|
-
sessionId: z.string().min(1),
|
|
105
|
-
sourceAdapter: z
|
|
106
|
-
|
|
107
|
-
|
|
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),
|
|
108
187
|
importance: z.number().int().min(1).max(10),
|
|
109
188
|
// OPTIONAL for backward-compat with exports predating team provenance (A3):
|
|
110
189
|
// older exports lack author/originProjectId, so the service stamps the local
|
|
@@ -112,10 +191,32 @@ export const importMemoryRecordSchema = z.object({
|
|
|
112
191
|
// `originProjectId: null` (and `author: ""`) for locally-authored rows — a
|
|
113
192
|
// team sync round-trips those exported snapshots verbatim, so the schema must
|
|
114
193
|
// accept the null the DTO carries rather than skip-and-warn every local row.
|
|
115
|
-
author
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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(),
|
|
119
220
|
});
|
|
120
221
|
export const importMemoriesRequestSchema = z.object({
|
|
121
222
|
projectId: z.string().min(1),
|
|
@@ -123,7 +224,7 @@ export const importMemoriesRequestSchema = z.object({
|
|
|
123
224
|
// the service layer can fall back to the policy-config redactionEnabled
|
|
124
225
|
// setting (override > config.json > default precedence).
|
|
125
226
|
redactionEnabled: z.boolean().optional(),
|
|
126
|
-
memories: z.array(importMemoryRecordSchema),
|
|
227
|
+
memories: z.array(importMemoryRecordSchema).max(MAX_IMPORT_SIZE),
|
|
127
228
|
});
|
|
128
229
|
// Team pull. Mirrors importMemoriesRequestSchema — the pull
|
|
129
230
|
// path reuses importMemoryRecordSchema verbatim (carrying author/originProjectId
|
|
@@ -133,7 +234,7 @@ export const importMemoriesRequestSchema = z.object({
|
|
|
133
234
|
export const pullMemoriesRequestSchema = z.object({
|
|
134
235
|
projectId: z.string().min(1),
|
|
135
236
|
redactionEnabled: z.boolean().optional(),
|
|
136
|
-
memories: z.array(importMemoryRecordSchema),
|
|
237
|
+
memories: z.array(importMemoryRecordSchema).max(MAX_IMPORT_SIZE),
|
|
137
238
|
});
|
|
138
239
|
export const statsRequestSchema = z.object({
|
|
139
240
|
projectId: z.string().min(1),
|
|
@@ -164,6 +265,13 @@ export const redactExistingResponseSchema = z.object({
|
|
|
164
265
|
skipped: z.number().int().nonnegative().default(0),
|
|
165
266
|
previews: z.array(z.string()),
|
|
166
267
|
});
|
|
268
|
+
export const resetAccessCountsRequestSchema = z.object({
|
|
269
|
+
projectId: z.string().min(1),
|
|
270
|
+
});
|
|
271
|
+
export const resetAccessCountsResponseSchema = z.object({
|
|
272
|
+
ok: z.literal(true),
|
|
273
|
+
affected: z.number().int().nonnegative(),
|
|
274
|
+
});
|
|
167
275
|
export const operationResultSchema = z.object({
|
|
168
276
|
ok: z.literal(true),
|
|
169
277
|
});
|
|
@@ -225,7 +333,7 @@ export const ingestSessionEventsResponseSchema = z.object({
|
|
|
225
333
|
});
|
|
226
334
|
export const summarizeSessionToMemoryResponseSchema = z.object({
|
|
227
335
|
ok: z.literal(true),
|
|
228
|
-
memoryId: z.string().min(1),
|
|
336
|
+
memoryId: z.string().min(1).max(200),
|
|
229
337
|
});
|
|
230
338
|
export const handleSessionEndResponseSchema = z.object({
|
|
231
339
|
ok: z.literal(true),
|
|
@@ -234,7 +342,7 @@ export const handleSessionEndResponseSchema = z.object({
|
|
|
234
342
|
warningCodes: z.array(z.string()),
|
|
235
343
|
warningMessages: z.array(z.string()),
|
|
236
344
|
failureRecordId: z.string().min(1).optional(),
|
|
237
|
-
memoryId: z.string().min(1).optional(),
|
|
345
|
+
memoryId: z.string().min(1).max(200).optional(),
|
|
238
346
|
});
|
|
239
347
|
export const importMemoriesResponseSchema = z.object({
|
|
240
348
|
ok: z.literal(true),
|
|
@@ -243,6 +351,10 @@ export const importMemoriesResponseSchema = z.object({
|
|
|
243
351
|
// different project's memory. These are never upserted, preventing
|
|
244
352
|
// cross-project overwrite/reassignment via ON CONFLICT(id).
|
|
245
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),
|
|
246
358
|
warningCodes: z.array(z.string()),
|
|
247
359
|
});
|
|
248
360
|
// Team pull response: distinguishes brand-new inserts from updates so the
|
|
@@ -255,9 +367,34 @@ export const pullMemoriesResponseSchema = z.object({
|
|
|
255
367
|
skippedCrossProject: z.number().int().nonnegative().default(0),
|
|
256
368
|
warningCodes: z.array(z.string()),
|
|
257
369
|
});
|
|
258
|
-
export const
|
|
370
|
+
export const batchStoreMemoryItemSchema = z.object({
|
|
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),
|
|
381
|
+
importance: z.number().int().min(1).max(10),
|
|
382
|
+
redactionEnabled: z.boolean().optional(),
|
|
383
|
+
});
|
|
384
|
+
export const batchStoreMemoryRequestSchema = z.object({
|
|
385
|
+
projectId: z.string().min(1),
|
|
386
|
+
memories: z.array(batchStoreMemoryItemSchema).min(1).max(MAX_BATCH_SIZE),
|
|
387
|
+
});
|
|
388
|
+
export const batchStoreMemoryResultSchema = z.object({
|
|
389
|
+
memoryId: z.string().min(1).max(200),
|
|
390
|
+
ok: z.boolean(),
|
|
391
|
+
memory: memorySchema.optional(),
|
|
392
|
+
warningCodes: z.array(z.string()).optional(),
|
|
393
|
+
error: z.string().optional(),
|
|
394
|
+
});
|
|
395
|
+
export const batchStoreMemoryResponseSchema = z.object({
|
|
259
396
|
ok: z.literal(true),
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
397
|
+
results: z.array(batchStoreMemoryResultSchema),
|
|
398
|
+
stored: z.number().int().nonnegative(),
|
|
399
|
+
failed: z.number().int().nonnegative(),
|
|
263
400
|
});
|
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
|
}
|