sessionmem 1.0.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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +344 -0
  3. package/dist/adapters/capabilities/fallbackTools.js +36 -0
  4. package/dist/adapters/contract/hostAdapterContract.js +1 -0
  5. package/dist/adapters/factory.js +40 -0
  6. package/dist/adapters/generic.js +128 -0
  7. package/dist/adapters/global/antigravity.js +22 -0
  8. package/dist/adapters/global/claudeCode.js +22 -0
  9. package/dist/adapters/global/codex.js +22 -0
  10. package/dist/adapters/global/qcoder.js +22 -0
  11. package/dist/adapters/ide/cline.js +20 -0
  12. package/dist/adapters/ide/cursor.js +28 -0
  13. package/dist/adapters/ide/installer.js +57 -0
  14. package/dist/adapters/ide/windsurf.js +28 -0
  15. package/dist/adapters/tools/ping.js +15 -0
  16. package/dist/cli/commands/config.js +79 -0
  17. package/dist/cli/commands/export.js +28 -0
  18. package/dist/cli/commands/forget.js +28 -0
  19. package/dist/cli/commands/import.js +112 -0
  20. package/dist/cli/commands/install.js +57 -0
  21. package/dist/cli/commands/list.js +13 -0
  22. package/dist/cli/commands/ping.js +12 -0
  23. package/dist/cli/commands/redactScan.js +40 -0
  24. package/dist/cli/commands/retention.js +54 -0
  25. package/dist/cli/commands/run.js +26 -0
  26. package/dist/cli/commands/search.js +29 -0
  27. package/dist/cli/commands/show.js +15 -0
  28. package/dist/cli/commands/stats.js +46 -0
  29. package/dist/cli/commands/sync.js +118 -0
  30. package/dist/cli/commands/team.js +96 -0
  31. package/dist/cli/commands/uninstall.js +30 -0
  32. package/dist/cli/context.js +69 -0
  33. package/dist/cli/index.js +147 -0
  34. package/dist/cli/output.js +37 -0
  35. package/dist/core/api/contracts.js +263 -0
  36. package/dist/core/api/errors.js +29 -0
  37. package/dist/core/api/localOnlyPolicy.js +29 -0
  38. package/dist/core/api/memoryCoreService.js +595 -0
  39. package/dist/core/api/sessionLifecycleService.js +289 -0
  40. package/dist/core/config/policyConfig.js +131 -0
  41. package/dist/core/embed/deterministicEmbed.js +31 -0
  42. package/dist/core/embed/embeddingVersion.js +1 -0
  43. package/dist/core/embed/reembedPolicy.js +9 -0
  44. package/dist/core/embed/textNormalize.js +12 -0
  45. package/dist/core/injection/formatStartupInjection.js +97 -0
  46. package/dist/core/injection/tokenBudget.js +38 -0
  47. package/dist/core/retrieve/decay.js +15 -0
  48. package/dist/core/retrieve/importance.js +6 -0
  49. package/dist/core/retrieve/recencyBands.js +18 -0
  50. package/dist/core/retrieve/retrieveMemories.js +83 -0
  51. package/dist/core/retrieve/score.js +25 -0
  52. package/dist/core/schema/migrations/001_initial.sql +25 -0
  53. package/dist/core/schema/migrations/002_indexes.sql +18 -0
  54. package/dist/core/schema/migrations/003_summarization_failures.sql +14 -0
  55. package/dist/core/schema/migrations/004_memory_feedback.sql +12 -0
  56. package/dist/core/schema/migrations/005_team_provenance.sql +9 -0
  57. package/dist/core/schema/runMigrations.js +38 -0
  58. package/dist/core/session.js +4 -0
  59. package/dist/core/storage/db.js +8 -0
  60. package/dist/core/storage/memoryFeedbackRepo.js +16 -0
  61. package/dist/core/storage/memoryRepo.js +179 -0
  62. package/dist/core/storage/memorySearchRepo.js +30 -0
  63. package/dist/core/storage/sessionEventsRepo.js +20 -0
  64. package/dist/core/storage/summarizationFailuresRepo.js +39 -0
  65. package/dist/core/storage/types.js +1 -0
  66. package/dist/core/summarize/cloudSummarizer.js +19 -0
  67. package/dist/core/summarize/localSummarizer.js +31 -0
  68. package/dist/core/summarize/redaction.js +48 -0
  69. package/dist/core/summarize/strategySelector.js +7 -0
  70. package/dist/core/summarize/summaryShape.js +49 -0
  71. package/package.json +48 -0
@@ -0,0 +1,83 @@
1
+ import { deterministicEmbed } from "../embed/deterministicEmbed.js";
2
+ import { scoreMemoryCandidate, } from "./score.js";
3
+ import { searchMemoryCandidates, } from "../storage/memorySearchRepo.js";
4
+ const DEFAULT_EMBEDDING_DIMENSION = 32;
5
+ function resolveEmbeddingDimension(candidates) {
6
+ for (const candidate of candidates) {
7
+ if (Number.isInteger(candidate.embedding_dim) && (candidate.embedding_dim ?? 0) > 0) {
8
+ return candidate.embedding_dim;
9
+ }
10
+ if (candidate.embedding && candidate.embedding.length > 0) {
11
+ return candidate.embedding.length;
12
+ }
13
+ }
14
+ return DEFAULT_EMBEDDING_DIMENSION;
15
+ }
16
+ function cosineSimilarity(query, candidate) {
17
+ if (!candidate || candidate.length !== query.length) {
18
+ return 0;
19
+ }
20
+ let dot = 0;
21
+ let queryMagnitude = 0;
22
+ let candidateMagnitude = 0;
23
+ for (let i = 0; i < query.length; i += 1) {
24
+ const queryValue = query[i];
25
+ const candidateValue = candidate[i];
26
+ dot += queryValue * candidateValue;
27
+ queryMagnitude += queryValue * queryValue;
28
+ candidateMagnitude += candidateValue * candidateValue;
29
+ }
30
+ if (queryMagnitude === 0 || candidateMagnitude === 0) {
31
+ return 0;
32
+ }
33
+ const similarity = dot / (Math.sqrt(queryMagnitude) * Math.sqrt(candidateMagnitude));
34
+ return Math.max(-1, Math.min(1, similarity));
35
+ }
36
+ export function retrieveMemories(input) {
37
+ const queryText = input.queryText ?? input.query;
38
+ if (!queryText) {
39
+ throw new Error("queryText is required");
40
+ }
41
+ const topK = input.topK ?? input.limit ?? 20;
42
+ const now = input.now ?? new Date();
43
+ const candidates = searchMemoryCandidates(input.db, input.projectId);
44
+ const dimension = resolveEmbeddingDimension(candidates);
45
+ const queryVector = deterministicEmbed(queryText, dimension).vector;
46
+ const ranked = candidates
47
+ .map((candidate) => {
48
+ const semantic = cosineSimilarity(queryVector, candidate.embedding);
49
+ const score = scoreMemoryCandidate({
50
+ semantic,
51
+ updated_at: candidate.updated_at,
52
+ importance: candidate.importance,
53
+ }, now);
54
+ return {
55
+ id: candidate.id,
56
+ project_id: candidate.project_id,
57
+ session_id: candidate.session_id,
58
+ source_adapter: candidate.source_adapter,
59
+ kind: candidate.kind,
60
+ content: candidate.content,
61
+ normalized_content: candidate.normalized_content,
62
+ importance: candidate.importance,
63
+ author: candidate.author,
64
+ origin_project_id: candidate.origin_project_id,
65
+ created_at: candidate.created_at,
66
+ updated_at: candidate.updated_at,
67
+ embedding_dim: candidate.embedding_dim,
68
+ embedding_version: candidate.embedding_version,
69
+ semantic,
70
+ score,
71
+ };
72
+ })
73
+ .sort((left, right) => {
74
+ if (right.score.total !== left.score.total) {
75
+ return right.score.total - left.score.total;
76
+ }
77
+ if (right.updated_at !== left.updated_at) {
78
+ return right.updated_at.localeCompare(left.updated_at);
79
+ }
80
+ return left.id.localeCompare(right.id);
81
+ });
82
+ return ranked.slice(0, topK);
83
+ }
@@ -0,0 +1,25 @@
1
+ import { normalizeImportance } from "./importance.js";
2
+ import { getRecencyBandScore } from "./recencyBands.js";
3
+ export const SCORING_WEIGHTS = {
4
+ semantic: 0.60,
5
+ recency: 0.25,
6
+ importance: 0.15,
7
+ };
8
+ export function scoreMemoryCandidate(candidate, now = new Date()) {
9
+ const recency = getRecencyBandScore(candidate.updated_at, now);
10
+ const importance = normalizeImportance(candidate.importance);
11
+ const weighted = {
12
+ semantic: candidate.semantic * SCORING_WEIGHTS.semantic,
13
+ recency: recency * SCORING_WEIGHTS.recency,
14
+ importance: importance * SCORING_WEIGHTS.importance,
15
+ };
16
+ return {
17
+ raw: {
18
+ semantic: candidate.semantic,
19
+ recency,
20
+ importance,
21
+ },
22
+ weighted,
23
+ total: weighted.semantic + weighted.recency + weighted.importance,
24
+ };
25
+ }
@@ -0,0 +1,25 @@
1
+ CREATE TABLE IF NOT EXISTS session_events (
2
+ id TEXT PRIMARY KEY,
3
+ project_id TEXT NOT NULL,
4
+ session_id TEXT NOT NULL,
5
+ event_index INTEGER NOT NULL,
6
+ event_type TEXT NOT NULL,
7
+ payload_json TEXT NOT NULL,
8
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
9
+ );
10
+
11
+ CREATE TABLE IF NOT EXISTS memories (
12
+ id TEXT PRIMARY KEY,
13
+ project_id TEXT NOT NULL,
14
+ session_id TEXT NOT NULL,
15
+ source_adapter TEXT NOT NULL,
16
+ kind TEXT NOT NULL,
17
+ content TEXT NOT NULL,
18
+ normalized_content TEXT NOT NULL,
19
+ importance INTEGER NOT NULL CHECK (importance >= 1 AND importance <= 10),
20
+ embedding TEXT,
21
+ embedding_dim INTEGER,
22
+ embedding_version TEXT,
23
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
24
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
25
+ );
@@ -0,0 +1,18 @@
1
+ CREATE INDEX IF NOT EXISTS idx_memories_project_updated
2
+ ON memories(project_id, updated_at DESC);
3
+
4
+ CREATE INDEX IF NOT EXISTS idx_memories_project_session
5
+ ON memories(project_id, session_id);
6
+
7
+ CREATE INDEX IF NOT EXISTS idx_memories_project_importance
8
+ ON memories(project_id, importance DESC);
9
+
10
+ CREATE INDEX IF NOT EXISTS idx_memories_project_created
11
+ ON memories(project_id, created_at DESC);
12
+
13
+ CREATE INDEX IF NOT EXISTS idx_session_events_project_session_event_index
14
+ ON session_events(project_id, session_id, event_index);
15
+
16
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_project_session_summary
17
+ ON memories(project_id, session_id, kind)
18
+ WHERE kind = 'summary';
@@ -0,0 +1,14 @@
1
+ CREATE TABLE IF NOT EXISTS summarization_failures (
2
+ id TEXT PRIMARY KEY,
3
+ project_id TEXT NOT NULL,
4
+ session_id TEXT NOT NULL,
5
+ source_adapter TEXT NOT NULL,
6
+ reason TEXT NOT NULL,
7
+ attempt_count INTEGER NOT NULL,
8
+ last_error_json TEXT NOT NULL,
9
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
10
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
11
+ );
12
+
13
+ CREATE INDEX IF NOT EXISTS idx_sum_fail_project_session
14
+ ON summarization_failures(project_id, session_id, updated_at DESC);
@@ -0,0 +1,12 @@
1
+ CREATE TABLE IF NOT EXISTS memory_feedback (
2
+ id TEXT PRIMARY KEY,
3
+ memory_id TEXT NOT NULL,
4
+ feedback_type TEXT NOT NULL CHECK (feedback_type IN ('auto_use', 'manual')),
5
+ previous_importance INTEGER NOT NULL CHECK (previous_importance >= 1 AND previous_importance <= 10),
6
+ new_importance INTEGER NOT NULL CHECK (new_importance >= 1 AND new_importance <= 10),
7
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
8
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
9
+ );
10
+
11
+ CREATE INDEX IF NOT EXISTS idx_memory_feedback_memory_created
12
+ ON memory_feedback(memory_id, created_at DESC);
@@ -0,0 +1,9 @@
1
+ -- Team-mode provenance: record who authored each memory and, for
2
+ -- synced/shared rows, which project they originated in. Both columns are added
3
+ -- to the existing `memories` table without rewriting it so pre-existing rows
4
+ -- survive. `author` is NOT NULL with a DEFAULT '' sentinel because a
5
+ -- NOT NULL ADD COLUMN requires a default and the local OS username is not
6
+ -- available inside static SQL. `origin_project_id` is nullable and
7
+ -- only set on rows pulled in from another project's store.
8
+ ALTER TABLE memories ADD COLUMN author TEXT NOT NULL DEFAULT '';
9
+ ALTER TABLE memories ADD COLUMN origin_project_id TEXT;
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const DEFAULT_MIGRATIONS_DIR = path.resolve(process.cwd(), "src/core/schema/migrations");
4
+ function ensureMigrationsTable(db) {
5
+ db.exec(`
6
+ CREATE TABLE IF NOT EXISTS _migrations (
7
+ name TEXT PRIMARY KEY,
8
+ applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
9
+ );
10
+ `);
11
+ }
12
+ function listMigrationFiles(migrationsDir) {
13
+ if (!fs.existsSync(migrationsDir)) {
14
+ return [];
15
+ }
16
+ return fs
17
+ .readdirSync(migrationsDir)
18
+ .filter((fileName) => fileName.endsWith(".sql"))
19
+ .sort((left, right) => left.localeCompare(right));
20
+ }
21
+ export function runMigrations(db, migrationsDir = DEFAULT_MIGRATIONS_DIR) {
22
+ ensureMigrationsTable(db);
23
+ const files = listMigrationFiles(migrationsDir);
24
+ const hasMigrationStmt = db.prepare("SELECT name FROM _migrations WHERE name = ? LIMIT 1");
25
+ const insertMigrationStmt = db.prepare("INSERT INTO _migrations(name) VALUES (?)");
26
+ const runMigration = db.transaction((fileName) => {
27
+ const filePath = path.join(migrationsDir, fileName);
28
+ const sql = fs.readFileSync(filePath, "utf8");
29
+ db.exec(sql);
30
+ insertMigrationStmt.run(fileName);
31
+ });
32
+ for (const fileName of files) {
33
+ const existing = hasMigrationStmt.get(fileName);
34
+ if (!existing) {
35
+ runMigration(fileName);
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,4 @@
1
+ export function onSessionDisconnect(adapterName, error) {
2
+ const reason = error ? error.message : "unknown cause";
3
+ console.warn(`[sessionmem] ${adapterName} disconnected mid-session (${reason}). Continuing.`);
4
+ }
@@ -0,0 +1,8 @@
1
+ import BetterSqlite3 from "better-sqlite3";
2
+ import { runMigrations } from "../schema/runMigrations.js";
3
+ export function openDb(options = {}) {
4
+ const db = new BetterSqlite3(options.dbPath ?? ":memory:");
5
+ db.pragma("foreign_keys = ON");
6
+ runMigrations(db, options.migrationsDir);
7
+ return db;
8
+ }
@@ -0,0 +1,16 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export function insertMemoryFeedbackEvent(db, event) {
3
+ const stmt = db.prepare(`
4
+ INSERT INTO memory_feedback (
5
+ id, memory_id, feedback_type, previous_importance, new_importance, created_at
6
+ ) VALUES (
7
+ @id, @memory_id, @feedback_type, @previous_importance, @new_importance,
8
+ COALESCE(@created_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
9
+ )
10
+ `);
11
+ stmt.run({
12
+ ...event,
13
+ id: event.id ?? randomUUID(),
14
+ created_at: event.created_at ?? null,
15
+ });
16
+ }
@@ -0,0 +1,179 @@
1
+ import { insertMemoryFeedbackEvent, } from "./memoryFeedbackRepo.js";
2
+ function assertImportance(importance) {
3
+ if (importance < 1 || importance > 10) {
4
+ throw new Error("importance must be between 1 and 10");
5
+ }
6
+ }
7
+ function toParams(input) {
8
+ return {
9
+ ...input,
10
+ embedding: input.embedding ?? null,
11
+ embedding_dim: input.embedding_dim ?? null,
12
+ embedding_version: input.embedding_version ?? null,
13
+ author: input.author ?? "",
14
+ origin_project_id: input.origin_project_id ?? null,
15
+ created_at: input.created_at ?? null,
16
+ updated_at: input.updated_at ?? null,
17
+ };
18
+ }
19
+ export function insertMemory(db, input) {
20
+ assertImportance(input.importance);
21
+ const stmt = db.prepare(`
22
+ INSERT INTO memories (
23
+ id, project_id, session_id, source_adapter, kind, content, normalized_content,
24
+ importance, embedding, embedding_dim, embedding_version, author, origin_project_id,
25
+ created_at, updated_at
26
+ ) VALUES (
27
+ @id, @project_id, @session_id, @source_adapter, @kind, @content, @normalized_content,
28
+ @importance, @embedding, @embedding_dim, @embedding_version, @author, @origin_project_id,
29
+ COALESCE(@created_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
30
+ COALESCE(@updated_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
31
+ )
32
+ `);
33
+ stmt.run(toParams(input));
34
+ }
35
+ export function upsertSessionSummaryMemory(db, input) {
36
+ assertImportance(input.importance);
37
+ const stmt = db.prepare(`
38
+ INSERT INTO memories (
39
+ id, project_id, session_id, source_adapter, kind, content, normalized_content,
40
+ importance, embedding, embedding_dim, embedding_version, author, origin_project_id,
41
+ created_at, updated_at
42
+ ) VALUES (
43
+ @id, @project_id, @session_id, @source_adapter, 'summary', @content, @normalized_content,
44
+ @importance, @embedding, @embedding_dim, @embedding_version, @author, @origin_project_id,
45
+ COALESCE(@created_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
46
+ COALESCE(@updated_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
47
+ )
48
+ ON CONFLICT(project_id, session_id, kind) WHERE kind = 'summary'
49
+ DO UPDATE SET
50
+ id = excluded.id,
51
+ source_adapter = excluded.source_adapter,
52
+ content = excluded.content,
53
+ normalized_content = excluded.normalized_content,
54
+ importance = excluded.importance,
55
+ embedding = excluded.embedding,
56
+ embedding_dim = excluded.embedding_dim,
57
+ embedding_version = excluded.embedding_version,
58
+ author = excluded.author,
59
+ origin_project_id = excluded.origin_project_id,
60
+ updated_at = COALESCE(excluded.updated_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
61
+ `);
62
+ stmt.run(toParams({ ...input, kind: "summary" }));
63
+ }
64
+ export function listMemoriesByProject(db, projectId) {
65
+ const stmt = db.prepare(`
66
+ SELECT
67
+ id, project_id, session_id, source_adapter, kind, content, normalized_content,
68
+ importance, embedding, embedding_dim, embedding_version, author, origin_project_id,
69
+ created_at, updated_at
70
+ FROM memories
71
+ WHERE project_id = ?
72
+ ORDER BY updated_at DESC
73
+ `);
74
+ return stmt.all(projectId);
75
+ }
76
+ /**
77
+ * All memory ids across every project. `id` is a globally-unique
78
+ * PRIMARY KEY, so duplicate-skip checks in `import` must consider every
79
+ * project's ids, not just the current project's, to surface cross-project id
80
+ * collisions as "skipped" rather than silently importing them.
81
+ */
82
+ export function listAllMemoryIds(db) {
83
+ const rows = db.prepare("SELECT id FROM memories").all();
84
+ return new Set(rows.map((r) => r.id));
85
+ }
86
+ export function countMemoriesOlderThan(db, projectId, cutoffIso) {
87
+ // created_at is stored as strftime('%Y-%m-%dT%H:%M:%fZ') text; lexicographic
88
+ // comparison against an ISO-8601 UTC cutoff is correct for this fixed format.
89
+ const row = db
90
+ .prepare(`
91
+ SELECT COUNT(*) AS count
92
+ FROM memories
93
+ WHERE project_id = ? AND created_at < ?
94
+ `)
95
+ .get(projectId, cutoffIso);
96
+ return row.count;
97
+ }
98
+ export function deleteMemoriesOlderThan(db, projectId, cutoffIso) {
99
+ // Hard-delete scoped to the memories table only; never touches
100
+ // session_events or memory_feedback. project_id and cutoff are bound, never
101
+ // string-concatenated, to prevent SQL injection.
102
+ const result = db
103
+ .prepare(`
104
+ DELETE FROM memories
105
+ WHERE project_id = ? AND created_at < ?
106
+ `)
107
+ .run(projectId, cutoffIso);
108
+ return result.changes;
109
+ }
110
+ export function updateMemoryImportance(db, projectId, memoryId, nextImportance, usedAt) {
111
+ assertImportance(nextImportance);
112
+ const result = db
113
+ .prepare(`
114
+ UPDATE memories
115
+ SET
116
+ importance = ?,
117
+ updated_at = COALESCE(?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
118
+ WHERE project_id = ? AND id = ?
119
+ `)
120
+ .run(nextImportance, usedAt ?? null, projectId, memoryId);
121
+ if (result.changes === 0) {
122
+ throw new Error(`Memory not found: ${memoryId}`);
123
+ }
124
+ }
125
+ export function updateMemoryContent(db, projectId, memoryId, newContent, newNormalizedContent) {
126
+ // In-place content rewrite for the one-time redaction scrub. All
127
+ // values are bound parameters — projectId, memoryId, and content are never
128
+ // string-concatenated — mirroring updateMemoryImportance to prevent SQL
129
+ // injection. normalized_content is only overwritten when a new
130
+ // value is supplied so embeddings stay consistent with the redacted text.
131
+ const result = db
132
+ .prepare(`
133
+ UPDATE memories
134
+ SET
135
+ content = ?,
136
+ normalized_content = COALESCE(?, normalized_content),
137
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
138
+ WHERE project_id = ? AND id = ?
139
+ `)
140
+ .run(newContent, newNormalizedContent ?? null, projectId, memoryId);
141
+ if (result.changes === 0) {
142
+ throw new Error(`Memory not found: ${memoryId}`);
143
+ }
144
+ }
145
+ export function recordUse(db, input) {
146
+ const transaction = db.transaction((txInput) => {
147
+ const memory = db
148
+ .prepare(`
149
+ SELECT id, importance
150
+ FROM memories
151
+ WHERE project_id = ? AND id = ?
152
+ LIMIT 1
153
+ `)
154
+ .get(txInput.project_id, txInput.memory_id);
155
+ if (!memory) {
156
+ throw new Error(`Memory not found: ${txInput.memory_id}`);
157
+ }
158
+ const feedbackType = txInput.feedback_type ?? "auto_use";
159
+ const nextImportance = txInput.next_importance ??
160
+ (feedbackType === "auto_use"
161
+ ? Math.min(memory.importance + 1, 9)
162
+ : memory.importance);
163
+ updateMemoryImportance(db, txInput.project_id, txInput.memory_id, nextImportance, txInput.used_at);
164
+ insertMemoryFeedbackEvent(db, {
165
+ id: txInput.feedback_id,
166
+ memory_id: txInput.memory_id,
167
+ feedback_type: feedbackType,
168
+ previous_importance: memory.importance,
169
+ new_importance: nextImportance,
170
+ created_at: txInput.used_at,
171
+ });
172
+ return {
173
+ memory_id: txInput.memory_id,
174
+ previous_importance: memory.importance,
175
+ new_importance: nextImportance,
176
+ };
177
+ });
178
+ return transaction(input);
179
+ }
@@ -0,0 +1,30 @@
1
+ function parseEmbedding(value) {
2
+ if (!value) {
3
+ return null;
4
+ }
5
+ try {
6
+ const parsed = JSON.parse(value);
7
+ if (!Array.isArray(parsed) || !parsed.every((item) => Number.isFinite(item))) {
8
+ return null;
9
+ }
10
+ return parsed;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function searchMemoryCandidates(db, projectId) {
17
+ const stmt = db.prepare(`
18
+ SELECT
19
+ id, project_id, session_id, source_adapter, kind, content, normalized_content,
20
+ importance, author, origin_project_id, created_at, updated_at, embedding,
21
+ embedding_dim, embedding_version
22
+ FROM memories
23
+ WHERE project_id = ?
24
+ `);
25
+ const rows = stmt.all(projectId);
26
+ return rows.map((row) => ({
27
+ ...row,
28
+ embedding: parseEmbedding(row.embedding),
29
+ }));
30
+ }
@@ -0,0 +1,20 @@
1
+ export function insertSessionEvent(db, input) {
2
+ const stmt = db.prepare(`
3
+ INSERT INTO session_events (
4
+ id, project_id, session_id, event_index, event_type, payload_json, created_at
5
+ ) VALUES (
6
+ @id, @project_id, @session_id, @event_index, @event_type, @payload_json,
7
+ COALESCE(@created_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
8
+ )
9
+ `);
10
+ stmt.run(input);
11
+ }
12
+ export function listSessionEventsBySession(db, projectId, sessionId) {
13
+ const stmt = db.prepare(`
14
+ SELECT id, project_id, session_id, event_index, event_type, payload_json, created_at
15
+ FROM session_events
16
+ WHERE project_id = ? AND session_id = ?
17
+ ORDER BY event_index ASC
18
+ `);
19
+ return stmt.all(projectId, sessionId);
20
+ }
@@ -0,0 +1,39 @@
1
+ function toParams(input) {
2
+ return {
3
+ ...input,
4
+ created_at: input.created_at ?? null,
5
+ updated_at: input.updated_at ?? null,
6
+ };
7
+ }
8
+ export function insertSummarizationFailure(db, input) {
9
+ const stmt = db.prepare(`
10
+ INSERT INTO summarization_failures (
11
+ id, project_id, session_id, source_adapter, reason, attempt_count, last_error_json, created_at, updated_at
12
+ ) VALUES (
13
+ @id, @project_id, @session_id, @source_adapter, @reason, @attempt_count, @last_error_json,
14
+ COALESCE(@created_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
15
+ COALESCE(@updated_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
16
+ )
17
+ `);
18
+ stmt.run(toParams(input));
19
+ }
20
+ export function listSummarizationFailures(db, projectId, sessionId) {
21
+ if (sessionId) {
22
+ const stmt = db.prepare(`
23
+ SELECT
24
+ id, project_id, session_id, source_adapter, reason, attempt_count, last_error_json, created_at, updated_at
25
+ FROM summarization_failures
26
+ WHERE project_id = ? AND session_id = ?
27
+ ORDER BY updated_at DESC
28
+ `);
29
+ return stmt.all(projectId, sessionId);
30
+ }
31
+ const stmt = db.prepare(`
32
+ SELECT
33
+ id, project_id, session_id, source_adapter, reason, attempt_count, last_error_json, created_at, updated_at
34
+ FROM summarization_failures
35
+ WHERE project_id = ?
36
+ ORDER BY updated_at DESC
37
+ `);
38
+ return stmt.all(projectId);
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { summarizeLocalSessionEvents } from "./localSummarizer.js";
2
+ const DEFAULT_CLOUD_MODEL = "claude-sonnet-4-20250514";
3
+ export async function summarizeWithCloud(input) {
4
+ if (!input.anthropicApiKey.trim()) {
5
+ throw new Error("Missing anthropicApiKey");
6
+ }
7
+ const result = await summarizeLocalSessionEvents({
8
+ events: input.events,
9
+ summaryTokenCap: input.summaryTokenCap,
10
+ redactionEnabled: input.redactionEnabled,
11
+ factMode: input.factMode,
12
+ redactionRules: input.redactionRules,
13
+ });
14
+ const modelTag = input.model ?? DEFAULT_CLOUD_MODEL;
15
+ return {
16
+ summary: `[model:${modelTag}] ${result.summary}`,
17
+ warningCodes: result.warningCodes,
18
+ };
19
+ }
@@ -0,0 +1,31 @@
1
+ import { normalizeEmbeddingText } from "../embed/textNormalize.js";
2
+ import { applyRedaction } from "./redaction.js";
3
+ import { buildStructuredSummary } from "./summaryShape.js";
4
+ function countTokens(text) {
5
+ return text.trim().split(/\s+/).filter(Boolean).length;
6
+ }
7
+ function capTokens(text, cap) {
8
+ const tokens = text.trim().split(/\s+/).filter(Boolean);
9
+ if (tokens.length <= cap) {
10
+ return text;
11
+ }
12
+ return `${tokens.slice(0, cap).join(" ")} ...`;
13
+ }
14
+ export async function summarizeLocalSessionEvents(input) {
15
+ const structured = buildStructuredSummary(input.events, {
16
+ factMode: input.factMode,
17
+ });
18
+ const redactionResult = applyRedaction(structured, {
19
+ redactionEnabled: input.redactionEnabled,
20
+ rules: input.redactionRules,
21
+ });
22
+ const normalized = normalizeEmbeddingText(redactionResult.text);
23
+ const capped = capTokens(normalized, input.summaryTokenCap);
24
+ if (countTokens(capped) > input.summaryTokenCap) {
25
+ throw new Error("summary exceeds summaryTokenCap");
26
+ }
27
+ return {
28
+ summary: capped,
29
+ warningCodes: redactionResult.warningCodes,
30
+ };
31
+ }
@@ -0,0 +1,48 @@
1
+ // Rule ordering note: structural multi-segment secrets (PEM private-key blocks,
2
+ // JWTs) run BEFORE the narrower single-token rules so a broad rule cannot redact
3
+ // a fragment of a larger secret and leave a partial body behind. All patterns are
4
+ // anchored with explicit literal prefixes/markers and use bounded quantifiers to
5
+ // avoid catastrophic backtracking (ReDoS).
6
+ function defaultRules() {
7
+ return [
8
+ // Email (original rule — unchanged).
9
+ (input) => input.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[REDACTED_EMAIL]"),
10
+ // PEM private key block: ----BEGIN <type> PRIVATE KEY---- ... ----END ... ----.
11
+ // Run before per-token rules so the whole block collapses to one placeholder.
12
+ (input) => input.replace(/-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]{0,40}PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]"),
13
+ // JWT: three base64url segments separated by dots, header begins with "eyJ".
14
+ (input) => input.replace(/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\b/g, "[REDACTED_JWT]"),
15
+ // AWS access key id: AKIA + 16 uppercase alphanumerics.
16
+ (input) => input.replace(/\bAKIA[A-Z0-9]{16}\b/g, "[REDACTED_AWS_KEY]"),
17
+ // GitHub tokens: ghp_/gho_/ghu_/ghs_/ghr_ + 36 alphanumerics.
18
+ (input) => input.replace(/\bgh[poushr]_[A-Za-z0-9]{36}\b/g, "[REDACTED_GITHUB_TOKEN]"),
19
+ // OpenAI-style API key (original rule — unchanged).
20
+ (input) => input.replace(/sk-[a-zA-Z0-9]{12,}/g, "[REDACTED_API_KEY]"),
21
+ // Bearer token header value: "Bearer <token>" (case-insensitive), token redacted.
22
+ (input) => input.replace(/\bBearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, "Bearer [REDACTED_BEARER_TOKEN]"),
23
+ // Connection-string assignment: password=/secret= value -> key kept, value redacted.
24
+ (input) => input.replace(/\b(password|secret)=([^\s"'&;]+)/gi, "$1=[REDACTED]"),
25
+ ];
26
+ }
27
+ export function applyRedaction(input, options) {
28
+ if (!options.redactionEnabled) {
29
+ return {
30
+ text: input,
31
+ warningCodes: [],
32
+ };
33
+ }
34
+ let text = input;
35
+ const warningCodes = [];
36
+ for (const rule of options.rules ?? defaultRules()) {
37
+ try {
38
+ text = rule(text);
39
+ }
40
+ catch {
41
+ warningCodes.push("redaction_partial_failure");
42
+ }
43
+ }
44
+ return {
45
+ text,
46
+ warningCodes,
47
+ };
48
+ }
@@ -0,0 +1,7 @@
1
+ export function resolveSummarizerMode(config) {
2
+ if (config.allowCloudSummarization === true &&
3
+ Boolean(config.anthropicApiKey)) {
4
+ return "cloud";
5
+ }
6
+ return "local";
7
+ }