sessionmem 1.0.4 → 1.0.6
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 +120 -0
- package/dist/adapters/generic.js +83 -12
- package/dist/adapters/tools/ping.js +4 -1
- package/dist/cli/commands/install.js +18 -1
- package/dist/cli/commands/reEmbed.js +47 -0
- package/dist/cli/commands/run.js +28 -2
- package/dist/cli/commands/savings.js +75 -0
- package/dist/cli/commands/uninstall.js +10 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/output.js +11 -3
- package/dist/core/api/contracts.js +34 -10
- package/dist/core/api/memoryCoreService.js +188 -86
- package/dist/core/api/sessionLifecycleService.js +12 -2
- package/dist/core/config/policyConfig.js +20 -0
- package/dist/core/injection/formatStartupInjection.js +2 -1
- package/dist/core/injection/tokenBudget.js +8 -0
- package/dist/core/retrieve/importance.js +4 -3
- package/dist/core/retrieve/recencyBands.js +3 -10
- package/dist/core/retrieve/retrieveMemories.js +17 -4
- package/dist/core/retrieve/score.js +11 -1
- package/dist/core/schema/migrations/005_team_provenance.sql +9 -9
- package/dist/core/schema/migrations/006_access_pattern_boosting.sql +5 -0
- package/dist/core/schema/migrations/007_feedback_manual_delete.sql +23 -0
- package/dist/core/schema/migrations/008_fts5_search.sql +33 -0
- package/dist/core/storage/db.js +6 -0
- package/dist/core/storage/memoryFeedbackRepo.js +14 -4
- package/dist/core/storage/memoryRepo.js +134 -120
- package/dist/core/storage/memorySearchRepo.js +87 -13
- package/dist/core/storage/sessionEventsRepo.js +19 -9
- package/dist/core/storage/summarizationFailuresRepo.js +36 -26
- package/dist/core/storage/tokenSavingsRepo.js +20 -0
- package/dist/core/summarize/cloudSummarizer.js +21 -5
- package/dist/core/summarize/localSummarizer.js +1 -10
- package/package.json +50 -48
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
|
" | " +
|
|
@@ -11,6 +11,9 @@ export const memorySchema = z.object({
|
|
|
11
11
|
embedding: z.string().nullable(),
|
|
12
12
|
embeddingDim: z.number().int().nullable(),
|
|
13
13
|
embeddingVersion: z.string().nullable(),
|
|
14
|
+
accessCount: z.number().int().nonnegative(),
|
|
15
|
+
lastAccessed: z.string().nullable(),
|
|
16
|
+
effectiveImportance: z.number().int().min(1).max(10),
|
|
14
17
|
createdAt: z.string().min(1),
|
|
15
18
|
updatedAt: z.string().min(1),
|
|
16
19
|
});
|
|
@@ -78,12 +81,6 @@ export const retrieveMemoriesRequestSchema = z.object({
|
|
|
78
81
|
mode: z.enum(["auto", "on-demand"]).default("auto"),
|
|
79
82
|
depth: z.enum(["default", "deep"]).default("default"),
|
|
80
83
|
});
|
|
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
84
|
export const listMemoriesRequestSchema = z.object({
|
|
88
85
|
projectId: z.string().min(1),
|
|
89
86
|
});
|
|
@@ -164,6 +161,13 @@ export const redactExistingResponseSchema = z.object({
|
|
|
164
161
|
skipped: z.number().int().nonnegative().default(0),
|
|
165
162
|
previews: z.array(z.string()),
|
|
166
163
|
});
|
|
164
|
+
export const resetAccessCountsRequestSchema = z.object({
|
|
165
|
+
projectId: z.string().min(1),
|
|
166
|
+
});
|
|
167
|
+
export const resetAccessCountsResponseSchema = z.object({
|
|
168
|
+
ok: z.literal(true),
|
|
169
|
+
affected: z.number().int().nonnegative(),
|
|
170
|
+
});
|
|
167
171
|
export const operationResultSchema = z.object({
|
|
168
172
|
ok: z.literal(true),
|
|
169
173
|
});
|
|
@@ -255,9 +259,29 @@ export const pullMemoriesResponseSchema = z.object({
|
|
|
255
259
|
skippedCrossProject: z.number().int().nonnegative().default(0),
|
|
256
260
|
warningCodes: z.array(z.string()),
|
|
257
261
|
});
|
|
258
|
-
export const
|
|
259
|
-
|
|
262
|
+
export const batchStoreMemoryItemSchema = z.object({
|
|
263
|
+
memoryId: z.string().min(1),
|
|
264
|
+
sessionId: z.string().min(1),
|
|
265
|
+
sourceAdapter: z.string().min(1),
|
|
266
|
+
kind: z.string().min(1),
|
|
267
|
+
content: z.string().min(1),
|
|
268
|
+
importance: z.number().int().min(1).max(10),
|
|
269
|
+
redactionEnabled: z.boolean().optional(),
|
|
270
|
+
});
|
|
271
|
+
export const batchStoreMemoryRequestSchema = z.object({
|
|
272
|
+
projectId: z.string().min(1),
|
|
273
|
+
memories: z.array(batchStoreMemoryItemSchema).min(1),
|
|
274
|
+
});
|
|
275
|
+
export const batchStoreMemoryResultSchema = z.object({
|
|
260
276
|
memoryId: z.string().min(1),
|
|
261
|
-
|
|
262
|
-
|
|
277
|
+
ok: z.boolean(),
|
|
278
|
+
memory: memorySchema.optional(),
|
|
279
|
+
warningCodes: z.array(z.string()).optional(),
|
|
280
|
+
error: z.string().optional(),
|
|
281
|
+
});
|
|
282
|
+
export const batchStoreMemoryResponseSchema = z.object({
|
|
283
|
+
ok: z.literal(true),
|
|
284
|
+
results: z.array(batchStoreMemoryResultSchema),
|
|
285
|
+
stored: z.number().int().nonnegative(),
|
|
286
|
+
failed: z.number().int().nonnegative(),
|
|
263
287
|
});
|
|
@@ -2,12 +2,14 @@ import { userInfo } from "node:os";
|
|
|
2
2
|
import { ZodError } from "zod";
|
|
3
3
|
import { deterministicEmbed } from "../embed/deterministicEmbed.js";
|
|
4
4
|
import { retrieveMemories } from "../retrieve/retrieveMemories.js";
|
|
5
|
+
import { computeEffectiveImportance } from "../retrieve/score.js";
|
|
5
6
|
import { formatStartupInjection } from "../injection/formatStartupInjection.js";
|
|
6
7
|
import { applyRedaction } from "../summarize/redaction.js";
|
|
7
|
-
import { countMemoriesOlderThan, deleteMemoriesOlderThan, insertMemory, listMemoriesByProject,
|
|
8
|
-
import { configFilePath, readPolicyConfig, resolvePolicySettings, } from "../config/policyConfig.js";
|
|
8
|
+
import { countMemoriesBySession, countMemoriesOlderThan, deleteMemoriesOlderThan, incrementAccessCounts, insertMemory, listMemoriesByProject, resetAccessCounts, updateMemoryContent, upsertSessionSummaryMemory, } from "../storage/memoryRepo.js";
|
|
9
|
+
import { SESSION_WRITE_SOFT_LIMIT, configFilePath, DEEP_MODE_RETRIEVAL_CAP, readPolicyConfig, resolvePolicySettings, } from "../config/policyConfig.js";
|
|
10
|
+
import { insertMemoryFeedbackEvent } from "../storage/memoryFeedbackRepo.js";
|
|
9
11
|
import { insertSessionEvent } from "../storage/sessionEventsRepo.js";
|
|
10
|
-
import { exportMemoriesRequestSchema, forgetMemoryRequestSchema, getMemoryRequestSchema, handleSessionEndRequestSchema, importMemoriesRequestSchema, pullMemoriesRequestSchema, ingestSessionEventsRequestSchema, listMemoriesRequestSchema, pruneMemoriesRequestSchema,
|
|
12
|
+
import { batchStoreMemoryItemSchema, batchStoreMemoryRequestSchema, exportMemoriesRequestSchema, forgetMemoryRequestSchema, getMemoryRequestSchema, handleSessionEndRequestSchema, importMemoriesRequestSchema, pullMemoriesRequestSchema, ingestSessionEventsRequestSchema, listMemoriesRequestSchema, pruneMemoriesRequestSchema, redactExistingRequestSchema, resetAccessCountsRequestSchema, retrieveMemoriesRequestSchema, statsRequestSchema, storeMemoryRequestSchema, summarizeSessionToMemoryRequestSchema, } from "./contracts.js";
|
|
11
13
|
import { DomainError, toErrorEnvelope } from "./errors.js";
|
|
12
14
|
import { assertLocalOnlyPolicy, } from "./localOnlyPolicy.js";
|
|
13
15
|
import { createSessionLifecycleService } from "./sessionLifecycleService.js";
|
|
@@ -48,6 +50,9 @@ function toMemoryDto(record) {
|
|
|
48
50
|
embeddingVersion: record.embedding_version,
|
|
49
51
|
author: record.author,
|
|
50
52
|
originProjectId: record.origin_project_id,
|
|
53
|
+
accessCount: record.access_count,
|
|
54
|
+
lastAccessed: record.last_accessed,
|
|
55
|
+
effectiveImportance: computeEffectiveImportance(record.importance, record.access_count),
|
|
51
56
|
createdAt: record.created_at,
|
|
52
57
|
updatedAt: record.updated_at,
|
|
53
58
|
};
|
|
@@ -67,6 +72,9 @@ function toRetrievedMemoryDto(record) {
|
|
|
67
72
|
embeddingVersion: record.embedding_version,
|
|
68
73
|
author: record.author,
|
|
69
74
|
originProjectId: record.origin_project_id,
|
|
75
|
+
accessCount: record.access_count,
|
|
76
|
+
lastAccessed: null,
|
|
77
|
+
effectiveImportance: computeEffectiveImportance(record.importance, record.access_count),
|
|
70
78
|
createdAt: record.created_at,
|
|
71
79
|
updatedAt: record.updated_at,
|
|
72
80
|
semantic: record.semantic,
|
|
@@ -75,14 +83,14 @@ function toRetrievedMemoryDto(record) {
|
|
|
75
83
|
}
|
|
76
84
|
function getMemoryById(db, projectId, memoryId) {
|
|
77
85
|
const row = db
|
|
78
|
-
.prepare(`
|
|
79
|
-
SELECT
|
|
80
|
-
id, project_id, session_id, source_adapter, kind, content, normalized_content,
|
|
81
|
-
importance, embedding, embedding_dim, embedding_version, author, origin_project_id,
|
|
82
|
-
created_at, updated_at
|
|
83
|
-
FROM memories
|
|
84
|
-
WHERE project_id = ? AND id = ?
|
|
85
|
-
LIMIT 1
|
|
86
|
+
.prepare(`
|
|
87
|
+
SELECT
|
|
88
|
+
id, project_id, session_id, source_adapter, kind, content, normalized_content,
|
|
89
|
+
importance, embedding, embedding_dim, embedding_version, author, origin_project_id,
|
|
90
|
+
access_count, last_accessed, created_at, updated_at
|
|
91
|
+
FROM memories
|
|
92
|
+
WHERE project_id = ? AND id = ?
|
|
93
|
+
LIMIT 1
|
|
86
94
|
`)
|
|
87
95
|
.get(projectId, memoryId);
|
|
88
96
|
return row;
|
|
@@ -188,6 +196,14 @@ export function createMemoryCoreService(deps) {
|
|
|
188
196
|
redactionEnabled: resolveRedactionEnabled(parsed.redactionEnabled),
|
|
189
197
|
});
|
|
190
198
|
const embedding = deterministicEmbed(redaction.text, dimension);
|
|
199
|
+
// Per-session write soft limit: warn the caller when the session has
|
|
200
|
+
// already accumulated SESSION_WRITE_SOFT_LIMIT memories so the agent
|
|
201
|
+
// gets feedback to stop storing excessively. The write still proceeds.
|
|
202
|
+
const warningCodes = [...redaction.warningCodes];
|
|
203
|
+
const sessionCount = countMemoriesBySession(db, parsed.sessionId);
|
|
204
|
+
if (sessionCount >= SESSION_WRITE_SOFT_LIMIT) {
|
|
205
|
+
warningCodes.push("session_write_limit_warning");
|
|
206
|
+
}
|
|
191
207
|
insertMemory(db, {
|
|
192
208
|
id: parsed.memoryId,
|
|
193
209
|
project_id: parsed.projectId,
|
|
@@ -212,45 +228,30 @@ export function createMemoryCoreService(deps) {
|
|
|
212
228
|
return {
|
|
213
229
|
ok: true,
|
|
214
230
|
memory: toMemoryDto(inserted),
|
|
215
|
-
warningCodes
|
|
231
|
+
warningCodes,
|
|
216
232
|
};
|
|
217
233
|
},
|
|
218
234
|
async retrieveMemories(request) {
|
|
219
235
|
const parsed = parseRequest(retrieveMemoriesRequestSchema, request);
|
|
220
|
-
const limit = parsed.depth === "deep" ? Math.min(parsed.limit * 2,
|
|
236
|
+
const limit = parsed.depth === "deep" ? Math.min(parsed.limit * 2, DEEP_MODE_RETRIEVAL_CAP) : parsed.limit;
|
|
221
237
|
const ranked = retrieveMemories({
|
|
222
238
|
db,
|
|
223
239
|
projectId: parsed.projectId,
|
|
224
240
|
queryText: parsed.query,
|
|
225
241
|
limit,
|
|
226
242
|
});
|
|
243
|
+
if (ranked.length > 0) {
|
|
244
|
+
incrementAccessCounts(db, parsed.projectId, ranked.map((m) => m.id));
|
|
245
|
+
}
|
|
227
246
|
return {
|
|
228
247
|
ok: true,
|
|
229
248
|
memories: ranked.map(toRetrievedMemoryDto),
|
|
230
249
|
total: ranked.length,
|
|
231
|
-
// Render the startup-injection block here so the `author:`
|
|
232
|
-
// prefix annotation for teammate-authored memories reaches CLI/MCP
|
|
233
|
-
// callers via the production retrieval path.
|
|
234
250
|
startupInjection: formatStartupInjection(ranked, {
|
|
235
251
|
localUsername: localAuthor,
|
|
236
252
|
}),
|
|
237
253
|
};
|
|
238
254
|
},
|
|
239
|
-
async recordMemoryUsed(request) {
|
|
240
|
-
const parsed = parseRequest(recordMemoryUsedRequestSchema, request);
|
|
241
|
-
const result = recordUse(db, {
|
|
242
|
-
project_id: parsed.projectId,
|
|
243
|
-
memory_id: parsed.memoryId,
|
|
244
|
-
feedback_type: parsed.feedbackType,
|
|
245
|
-
used_at: parsed.usedAt,
|
|
246
|
-
});
|
|
247
|
-
return {
|
|
248
|
-
ok: true,
|
|
249
|
-
memoryId: result.memory_id,
|
|
250
|
-
previousImportance: result.previous_importance,
|
|
251
|
-
newImportance: result.new_importance,
|
|
252
|
-
};
|
|
253
|
-
},
|
|
254
255
|
async listMemories(request) {
|
|
255
256
|
const parsed = parseRequest(listMemoriesRequestSchema, request);
|
|
256
257
|
const memories = listMemoriesByProject(db, parsed.projectId);
|
|
@@ -273,12 +274,24 @@ export function createMemoryCoreService(deps) {
|
|
|
273
274
|
},
|
|
274
275
|
async forgetMemory(request) {
|
|
275
276
|
const parsed = parseRequest(forgetMemoryRequestSchema, request);
|
|
277
|
+
// Capture the memory's importance before deletion so we can record
|
|
278
|
+
// it in the feedback table as an analytics signal.
|
|
279
|
+
const existing = getMemoryById(db, parsed.projectId, parsed.memoryId);
|
|
276
280
|
const result = db
|
|
277
281
|
.prepare("DELETE FROM memories WHERE project_id = ? AND id = ?")
|
|
278
282
|
.run(parsed.projectId, parsed.memoryId);
|
|
279
283
|
if (result.changes === 0) {
|
|
280
284
|
throw new DomainError("NOT_FOUND", `Memory not found: ${parsed.memoryId}`);
|
|
281
285
|
}
|
|
286
|
+
// Record the explicit user deletion as feedback. The FK on
|
|
287
|
+
// memory_feedback no longer cascades (migration 006), so this row
|
|
288
|
+
// survives the memory deletion and serves as an analytics signal.
|
|
289
|
+
insertMemoryFeedbackEvent(db, {
|
|
290
|
+
memory_id: parsed.memoryId,
|
|
291
|
+
feedback_type: "manual_delete",
|
|
292
|
+
previous_importance: existing?.importance ?? 0,
|
|
293
|
+
new_importance: 0,
|
|
294
|
+
});
|
|
282
295
|
return {
|
|
283
296
|
ok: true,
|
|
284
297
|
};
|
|
@@ -293,32 +306,32 @@ export function createMemoryCoreService(deps) {
|
|
|
293
306
|
},
|
|
294
307
|
async importMemories(request) {
|
|
295
308
|
const parsed = parseRequest(importMemoriesRequestSchema, request);
|
|
296
|
-
const stmt = db.prepare(`
|
|
297
|
-
INSERT INTO memories (
|
|
298
|
-
id, project_id, session_id, source_adapter, kind, content, normalized_content,
|
|
299
|
-
importance, embedding, embedding_dim, embedding_version, author, origin_project_id,
|
|
300
|
-
created_at, updated_at
|
|
301
|
-
) VALUES (
|
|
302
|
-
@id, @project_id, @session_id, @source_adapter, @kind, @content, @normalized_content,
|
|
303
|
-
@importance, @embedding, @embedding_dim, @embedding_version, @author, @origin_project_id,
|
|
304
|
-
COALESCE(@created_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
305
|
-
COALESCE(@updated_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
306
|
-
)
|
|
307
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
308
|
-
project_id = excluded.project_id,
|
|
309
|
-
session_id = excluded.session_id,
|
|
310
|
-
source_adapter = excluded.source_adapter,
|
|
311
|
-
kind = excluded.kind,
|
|
312
|
-
content = excluded.content,
|
|
313
|
-
normalized_content = excluded.normalized_content,
|
|
314
|
-
importance = excluded.importance,
|
|
315
|
-
embedding = excluded.embedding,
|
|
316
|
-
embedding_dim = excluded.embedding_dim,
|
|
317
|
-
embedding_version = excluded.embedding_version,
|
|
318
|
-
author = excluded.author,
|
|
319
|
-
origin_project_id = excluded.origin_project_id,
|
|
320
|
-
created_at = excluded.created_at,
|
|
321
|
-
updated_at = excluded.updated_at
|
|
309
|
+
const stmt = db.prepare(`
|
|
310
|
+
INSERT INTO memories (
|
|
311
|
+
id, project_id, session_id, source_adapter, kind, content, normalized_content,
|
|
312
|
+
importance, embedding, embedding_dim, embedding_version, author, origin_project_id,
|
|
313
|
+
created_at, updated_at
|
|
314
|
+
) VALUES (
|
|
315
|
+
@id, @project_id, @session_id, @source_adapter, @kind, @content, @normalized_content,
|
|
316
|
+
@importance, @embedding, @embedding_dim, @embedding_version, @author, @origin_project_id,
|
|
317
|
+
COALESCE(@created_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
318
|
+
COALESCE(@updated_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
319
|
+
)
|
|
320
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
321
|
+
project_id = excluded.project_id,
|
|
322
|
+
session_id = excluded.session_id,
|
|
323
|
+
source_adapter = excluded.source_adapter,
|
|
324
|
+
kind = excluded.kind,
|
|
325
|
+
content = excluded.content,
|
|
326
|
+
normalized_content = excluded.normalized_content,
|
|
327
|
+
importance = excluded.importance,
|
|
328
|
+
embedding = excluded.embedding,
|
|
329
|
+
embedding_dim = excluded.embedding_dim,
|
|
330
|
+
embedding_version = excluded.embedding_version,
|
|
331
|
+
author = excluded.author,
|
|
332
|
+
origin_project_id = excluded.origin_project_id,
|
|
333
|
+
created_at = excluded.created_at,
|
|
334
|
+
updated_at = excluded.updated_at
|
|
322
335
|
`);
|
|
323
336
|
// `id` is a globally-unique PRIMARY KEY (not scoped by
|
|
324
337
|
// project_id). The upsert above reassigns `project_id = excluded.project_id`
|
|
@@ -393,35 +406,35 @@ export function createMemoryCoreService(deps) {
|
|
|
393
406
|
// provenance so pulled rows carry the teammate's identity and
|
|
394
407
|
// their source project_id.
|
|
395
408
|
// - cross-project id collisions are skipped, exactly as import.
|
|
396
|
-
const stmt = db.prepare(`
|
|
397
|
-
INSERT INTO memories (
|
|
398
|
-
id, project_id, session_id, source_adapter, kind, content, normalized_content,
|
|
399
|
-
importance, embedding, embedding_dim, embedding_version, author, origin_project_id,
|
|
400
|
-
created_at, updated_at
|
|
401
|
-
) VALUES (
|
|
402
|
-
@id, @project_id, @session_id, @source_adapter, @kind, @content, @normalized_content,
|
|
403
|
-
@importance, @embedding, @embedding_dim, @embedding_version, @author, @origin_project_id,
|
|
404
|
-
COALESCE(@created_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
405
|
-
COALESCE(@updated_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
406
|
-
)
|
|
407
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
408
|
-
project_id = excluded.project_id,
|
|
409
|
-
session_id = excluded.session_id,
|
|
410
|
-
source_adapter = excluded.source_adapter,
|
|
411
|
-
kind = excluded.kind,
|
|
412
|
-
content = excluded.content,
|
|
413
|
-
normalized_content = excluded.normalized_content,
|
|
414
|
-
-- Importance-preserving merge. better-sqlite3@12 bundles a
|
|
415
|
-
-- SQLite that accepts the two-arg scalar MAX() inside DO UPDATE; the
|
|
416
|
-
-- pull-merge importance-preserve test verifies both directions.
|
|
417
|
-
importance = MAX(memories.importance, excluded.importance),
|
|
418
|
-
embedding = excluded.embedding,
|
|
419
|
-
embedding_dim = excluded.embedding_dim,
|
|
420
|
-
embedding_version = excluded.embedding_version,
|
|
421
|
-
author = excluded.author,
|
|
422
|
-
origin_project_id = excluded.origin_project_id,
|
|
423
|
-
created_at = excluded.created_at,
|
|
424
|
-
updated_at = excluded.updated_at
|
|
409
|
+
const stmt = db.prepare(`
|
|
410
|
+
INSERT INTO memories (
|
|
411
|
+
id, project_id, session_id, source_adapter, kind, content, normalized_content,
|
|
412
|
+
importance, embedding, embedding_dim, embedding_version, author, origin_project_id,
|
|
413
|
+
created_at, updated_at
|
|
414
|
+
) VALUES (
|
|
415
|
+
@id, @project_id, @session_id, @source_adapter, @kind, @content, @normalized_content,
|
|
416
|
+
@importance, @embedding, @embedding_dim, @embedding_version, @author, @origin_project_id,
|
|
417
|
+
COALESCE(@created_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
418
|
+
COALESCE(@updated_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
419
|
+
)
|
|
420
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
421
|
+
project_id = excluded.project_id,
|
|
422
|
+
session_id = excluded.session_id,
|
|
423
|
+
source_adapter = excluded.source_adapter,
|
|
424
|
+
kind = excluded.kind,
|
|
425
|
+
content = excluded.content,
|
|
426
|
+
normalized_content = excluded.normalized_content,
|
|
427
|
+
-- Importance-preserving merge. better-sqlite3@12 bundles a
|
|
428
|
+
-- SQLite that accepts the two-arg scalar MAX() inside DO UPDATE; the
|
|
429
|
+
-- pull-merge importance-preserve test verifies both directions.
|
|
430
|
+
importance = MAX(memories.importance, excluded.importance),
|
|
431
|
+
embedding = excluded.embedding,
|
|
432
|
+
embedding_dim = excluded.embedding_dim,
|
|
433
|
+
embedding_version = excluded.embedding_version,
|
|
434
|
+
author = excluded.author,
|
|
435
|
+
origin_project_id = excluded.origin_project_id,
|
|
436
|
+
created_at = excluded.created_at,
|
|
437
|
+
updated_at = excluded.updated_at
|
|
425
438
|
`);
|
|
426
439
|
// Same cross-project ownership skip as importMemories. A colliding
|
|
427
440
|
// id owned by a different project is skipped, never overwritten/relocated.
|
|
@@ -565,6 +578,87 @@ export function createMemoryCoreService(deps) {
|
|
|
565
578
|
previews,
|
|
566
579
|
};
|
|
567
580
|
},
|
|
581
|
+
async batchStoreMemory(request) {
|
|
582
|
+
const parsed = parseRequest(batchStoreMemoryRequestSchema, request);
|
|
583
|
+
const results = [];
|
|
584
|
+
let stored = 0;
|
|
585
|
+
let failed = 0;
|
|
586
|
+
// Validate each item individually before entering the transaction so
|
|
587
|
+
// validation errors are reported per-item without aborting the whole batch.
|
|
588
|
+
const validatedItems = [];
|
|
589
|
+
for (let i = 0; i < parsed.memories.length; i++) {
|
|
590
|
+
const raw = parsed.memories[i];
|
|
591
|
+
try {
|
|
592
|
+
// The array items were already parsed by batchStoreMemoryRequestSchema,
|
|
593
|
+
// but we re-validate with the item schema so per-item errors are
|
|
594
|
+
// captured individually (e.g. if a caller bypasses the outer schema).
|
|
595
|
+
const item = batchStoreMemoryItemSchema.parse(raw);
|
|
596
|
+
validatedItems.push({ index: i, item });
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
results.push({
|
|
600
|
+
memoryId: raw.memoryId ?? `<index-${i}>`,
|
|
601
|
+
ok: false,
|
|
602
|
+
error: err instanceof ZodError
|
|
603
|
+
? err.issues.map((issue) => issue.message).join("; ")
|
|
604
|
+
: String(err),
|
|
605
|
+
});
|
|
606
|
+
failed += 1;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// Wrap all valid inserts in a single SQLite transaction for atomicity
|
|
610
|
+
// and performance (better-sqlite3 transactions avoid per-statement
|
|
611
|
+
// fsync, making batch inserts significantly faster).
|
|
612
|
+
if (validatedItems.length > 0) {
|
|
613
|
+
const runTransaction = db.transaction(() => {
|
|
614
|
+
for (const { item } of validatedItems) {
|
|
615
|
+
const redaction = applyRedaction(item.content, {
|
|
616
|
+
redactionEnabled: resolveRedactionEnabled(item.redactionEnabled),
|
|
617
|
+
});
|
|
618
|
+
const embedding = deterministicEmbed(redaction.text, dimension);
|
|
619
|
+
insertMemory(db, {
|
|
620
|
+
id: item.memoryId,
|
|
621
|
+
project_id: parsed.projectId,
|
|
622
|
+
session_id: item.sessionId,
|
|
623
|
+
source_adapter: item.sourceAdapter,
|
|
624
|
+
kind: item.kind,
|
|
625
|
+
content: redaction.text,
|
|
626
|
+
normalized_content: embedding.normalizedText,
|
|
627
|
+
importance: item.importance,
|
|
628
|
+
embedding: JSON.stringify(embedding.vector),
|
|
629
|
+
embedding_dim: embedding.dimension,
|
|
630
|
+
embedding_version: embedding.embeddingVersion,
|
|
631
|
+
author: localAuthor,
|
|
632
|
+
origin_project_id: null,
|
|
633
|
+
});
|
|
634
|
+
const inserted = getMemoryById(db, parsed.projectId, item.memoryId);
|
|
635
|
+
if (!inserted) {
|
|
636
|
+
throw new DomainError("INTERNAL", `Memory insert did not persist: ${item.memoryId}`);
|
|
637
|
+
}
|
|
638
|
+
results.push({
|
|
639
|
+
memoryId: item.memoryId,
|
|
640
|
+
ok: true,
|
|
641
|
+
memory: toMemoryDto(inserted),
|
|
642
|
+
warningCodes: redaction.warningCodes,
|
|
643
|
+
});
|
|
644
|
+
stored += 1;
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
runTransaction();
|
|
648
|
+
}
|
|
649
|
+
// Sort results back into original input order: validated items were
|
|
650
|
+
// processed in order but failed items were pushed first. Re-sort by
|
|
651
|
+
// the memoryId to maintain a predictable output. Since memoryIds are
|
|
652
|
+
// unique, use the input array order as the canonical sort key.
|
|
653
|
+
const inputOrder = new Map(parsed.memories.map((m, i) => [m.memoryId, i]));
|
|
654
|
+
results.sort((a, b) => (inputOrder.get(a.memoryId) ?? 0) - (inputOrder.get(b.memoryId) ?? 0));
|
|
655
|
+
return {
|
|
656
|
+
ok: true,
|
|
657
|
+
results,
|
|
658
|
+
stored,
|
|
659
|
+
failed,
|
|
660
|
+
};
|
|
661
|
+
},
|
|
568
662
|
async stats(request) {
|
|
569
663
|
const parsed = parseRequest(statsRequestSchema, request);
|
|
570
664
|
const memoryCount = db
|
|
@@ -579,6 +673,14 @@ export function createMemoryCoreService(deps) {
|
|
|
579
673
|
totalSessionEvents: sessionEventCount.count,
|
|
580
674
|
};
|
|
581
675
|
},
|
|
676
|
+
async resetAccessCounts(request) {
|
|
677
|
+
const parsed = parseRequest(resetAccessCountsRequestSchema, request);
|
|
678
|
+
const affected = resetAccessCounts(db, parsed.projectId);
|
|
679
|
+
return {
|
|
680
|
+
ok: true,
|
|
681
|
+
affected,
|
|
682
|
+
};
|
|
683
|
+
},
|
|
582
684
|
};
|
|
583
685
|
async function call(method, request) {
|
|
584
686
|
try {
|
|
@@ -185,8 +185,18 @@ export function createSessionLifecycleService(deps) {
|
|
|
185
185
|
memoryId,
|
|
186
186
|
};
|
|
187
187
|
}
|
|
188
|
-
catch {
|
|
189
|
-
|
|
188
|
+
catch (cloudError) {
|
|
189
|
+
const failureRecordId = createFailureId();
|
|
190
|
+
insertSummarizationFailure(deps.db, {
|
|
191
|
+
id: failureRecordId,
|
|
192
|
+
project_id: request.projectId,
|
|
193
|
+
session_id: request.sessionId,
|
|
194
|
+
source_adapter: request.sourceAdapter,
|
|
195
|
+
reason: "cloud_failed",
|
|
196
|
+
attempt_count: CLOUD_RETRY_CONFIG.retries + 1,
|
|
197
|
+
last_error_json: toErrorJson(cloudError),
|
|
198
|
+
});
|
|
199
|
+
// fall through to local summarizer
|
|
190
200
|
}
|
|
191
201
|
try {
|
|
192
202
|
const fallbackResult = await summarizeLocal(baseInput);
|
|
@@ -2,6 +2,19 @@ import { z } from "zod";
|
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join, dirname } from "path";
|
|
4
4
|
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
/** Lower bound of the 1-10 importance scale. */
|
|
6
|
+
export const MIN_IMPORTANCE = 1;
|
|
7
|
+
/** Upper bound of the 1-10 importance scale. */
|
|
8
|
+
export const MAX_IMPORTANCE = 10;
|
|
9
|
+
/** Importance threshold at or above which a warning is considered critical. */
|
|
10
|
+
export const CRITICAL_WARNING_IMPORTANCE_THRESHOLD = 9;
|
|
11
|
+
/** Maximum number of memories returned by a "deep" retrieval. */
|
|
12
|
+
export const DEEP_MODE_RETRIEVAL_CAP = 100;
|
|
13
|
+
/**
|
|
14
|
+
* Default model for cloud summarization via the Anthropic API.
|
|
15
|
+
* Consumed by {@link import("../summarize/cloudSummarizer.js").summarizeWithCloud}.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_SUMMARIZER_MODEL = "claude-sonnet-4-6";
|
|
5
18
|
/**
|
|
6
19
|
* Built-in policy defaults. Used whenever the config file is missing, malformed,
|
|
7
20
|
* or fails validation, and as the lowest-precedence source in
|
|
@@ -116,6 +129,13 @@ export function resolvePolicySettings(input) {
|
|
|
116
129
|
redactionEnabled: resolve("redactionEnabled"),
|
|
117
130
|
};
|
|
118
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Per-session write soft limit. When a session has stored at least this many
|
|
134
|
+
* memories, subsequent storeMemory calls still succeed but the response
|
|
135
|
+
* includes a "session_write_limit_warning" warningCode, giving the agent
|
|
136
|
+
* feedback to stop storing excessive memories in a single session.
|
|
137
|
+
*/
|
|
138
|
+
export const SESSION_WRITE_SOFT_LIMIT = 50;
|
|
119
139
|
/**
|
|
120
140
|
* Resolve the `team` config as a single object unit using precedence
|
|
121
141
|
* override > config.json > default (RESEARCH Pitfall 5). Unlike
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CRITICAL_WARNING_IMPORTANCE_THRESHOLD } from "../config/policyConfig.js";
|
|
1
2
|
import { countTokens, trimLowestPriorityContent } from "./tokenBudget.js";
|
|
2
3
|
const DEFAULT_TOKEN_CAP = 450;
|
|
3
4
|
const HEADER = "Relevant prior context";
|
|
@@ -7,7 +8,7 @@ function kindRank(kind) {
|
|
|
7
8
|
return KIND_RANK.get(kind) ?? KIND_ORDER.length;
|
|
8
9
|
}
|
|
9
10
|
function isCriticalWarning(memory) {
|
|
10
|
-
return memory.kind === "warning" && memory.importance >=
|
|
11
|
+
return memory.kind === "warning" && memory.importance >= CRITICAL_WARNING_IMPORTANCE_THRESHOLD;
|
|
11
12
|
}
|
|
12
13
|
function sortMemories(memories) {
|
|
13
14
|
return [...memories].sort((left, right) => {
|
|
@@ -5,6 +5,14 @@ const DEFAULT_TRIM_RATIO = 0.75;
|
|
|
5
5
|
export function countTokens(text) {
|
|
6
6
|
return encoding.encode(text).length;
|
|
7
7
|
}
|
|
8
|
+
export function capTokens(text, cap) {
|
|
9
|
+
const tokens = encoding.encode(text);
|
|
10
|
+
if (tokens.length <= cap) {
|
|
11
|
+
return text;
|
|
12
|
+
}
|
|
13
|
+
const trimmed = encoding.decode(tokens.slice(0, cap)).trimEnd();
|
|
14
|
+
return `${trimmed} ...`;
|
|
15
|
+
}
|
|
8
16
|
export function trimLowestPriorityContent(included, options = {}) {
|
|
9
17
|
const minContentTokens = options.minContentTokens ?? DEFAULT_MIN_CONTENT_TOKENS;
|
|
10
18
|
const trimRatio = options.trimRatio ?? DEFAULT_TRIM_RATIO;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { MIN_IMPORTANCE, MAX_IMPORTANCE } from "../config/policyConfig.js";
|
|
1
2
|
export function normalizeImportance(score1to10) {
|
|
2
|
-
if (!Number.isFinite(score1to10) || score1to10 <
|
|
3
|
-
throw new Error(
|
|
3
|
+
if (!Number.isFinite(score1to10) || score1to10 < MIN_IMPORTANCE || score1to10 > MAX_IMPORTANCE) {
|
|
4
|
+
throw new Error(`importance must be between ${MIN_IMPORTANCE} and ${MAX_IMPORTANCE}`);
|
|
4
5
|
}
|
|
5
|
-
return (score1to10 -
|
|
6
|
+
return (score1to10 - MIN_IMPORTANCE) / (MAX_IMPORTANCE - MIN_IMPORTANCE);
|
|
6
7
|
}
|
|
@@ -5,14 +5,7 @@ function toDate(value) {
|
|
|
5
5
|
export function getRecencyBandScore(updatedAt, now = new Date()) {
|
|
6
6
|
const updatedDate = toDate(updatedAt);
|
|
7
7
|
const ageDays = Math.max(0, (now.getTime() - updatedDate.getTime()) / DAY_IN_MS);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (ageDays <= 7) {
|
|
12
|
-
return 0.75;
|
|
13
|
-
}
|
|
14
|
-
if (ageDays <= 30) {
|
|
15
|
-
return 0.5;
|
|
16
|
-
}
|
|
17
|
-
return 0.25;
|
|
8
|
+
const HALF_LIFE_DAYS = 14;
|
|
9
|
+
const lambda = Math.LN2 / HALF_LIFE_DAYS;
|
|
10
|
+
return Math.max(0.05, Math.exp(-lambda * ageDays));
|
|
18
11
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { deterministicEmbed } from "../embed/deterministicEmbed.js";
|
|
2
|
+
import { decayOldBoosts } from "./decay.js";
|
|
2
3
|
import { scoreMemoryCandidate, } from "./score.js";
|
|
3
|
-
import { searchMemoryCandidates, } from "../storage/memorySearchRepo.js";
|
|
4
|
+
import { searchMemoryCandidates, searchMemoryCandidatesFTS, } from "../storage/memorySearchRepo.js";
|
|
4
5
|
const DEFAULT_EMBEDDING_DIMENSION = 32;
|
|
5
6
|
function resolveEmbeddingDimension(candidates) {
|
|
6
7
|
for (const candidate of candidates) {
|
|
@@ -40,16 +41,27 @@ export function retrieveMemories(input) {
|
|
|
40
41
|
}
|
|
41
42
|
const topK = input.topK ?? input.limit ?? 20;
|
|
42
43
|
const now = input.now ?? new Date();
|
|
43
|
-
|
|
44
|
+
// Use FTS5 pre-filtering when a semantic query is present to limit
|
|
45
|
+
// cosine similarity computation to ~50 candidates instead of all.
|
|
46
|
+
const candidates = queryText
|
|
47
|
+
? searchMemoryCandidatesFTS(input.db, input.projectId, queryText)
|
|
48
|
+
: searchMemoryCandidates(input.db, input.projectId);
|
|
49
|
+
const decayedCandidates = decayOldBoosts(candidates, now);
|
|
44
50
|
const dimension = resolveEmbeddingDimension(candidates);
|
|
45
51
|
const queryVector = deterministicEmbed(queryText, dimension).vector;
|
|
46
|
-
const ranked =
|
|
52
|
+
const ranked = decayedCandidates
|
|
47
53
|
.map((candidate) => {
|
|
48
|
-
|
|
54
|
+
// When embedding is null (version mismatch or missing), use a neutral
|
|
55
|
+
// score of 0.5 so the memory is neither penalized nor boosted.
|
|
56
|
+
const semantic = candidate.embedding === null
|
|
57
|
+
? 0.5
|
|
58
|
+
: cosineSimilarity(queryVector, candidate.embedding);
|
|
49
59
|
const score = scoreMemoryCandidate({
|
|
50
60
|
semantic,
|
|
51
61
|
updated_at: candidate.updated_at,
|
|
52
62
|
importance: candidate.importance,
|
|
63
|
+
access_count: candidate.access_count,
|
|
64
|
+
decayedImportance: candidate.decayedImportance,
|
|
53
65
|
}, now);
|
|
54
66
|
return {
|
|
55
67
|
id: candidate.id,
|
|
@@ -62,6 +74,7 @@ export function retrieveMemories(input) {
|
|
|
62
74
|
importance: candidate.importance,
|
|
63
75
|
author: candidate.author,
|
|
64
76
|
origin_project_id: candidate.origin_project_id,
|
|
77
|
+
access_count: candidate.access_count,
|
|
65
78
|
created_at: candidate.created_at,
|
|
66
79
|
updated_at: candidate.updated_at,
|
|
67
80
|
embedding_dim: candidate.embedding_dim,
|