cogmem 3.6.5 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/MEMORY_ATLAS.md +59 -12
  3. package/README.md +21 -13
  4. package/RELEASE_CHECKLIST.md +5 -4
  5. package/dist/agent/AgentMemoryBackend.d.ts +24 -3
  6. package/dist/agent/AgentMemoryBackend.js +285 -55
  7. package/dist/agent/AgentRecallQueryCompiler.d.ts +1 -1
  8. package/dist/agent/AgentRecallQueryCompiler.js +13 -1
  9. package/dist/agent/MemoryUsageReceipt.js +8 -10
  10. package/dist/agent/SessionWorkingState.js +3 -2
  11. package/dist/agent/UntrustedMemorySerializer.d.ts +2 -0
  12. package/dist/agent/UntrustedMemorySerializer.js +14 -0
  13. package/dist/atlas/ActionFrameExtractor.js +2 -0
  14. package/dist/atlas/EpisodeTitleGenerator.d.ts +31 -0
  15. package/dist/atlas/EpisodeTitleGenerator.js +191 -0
  16. package/dist/atlas/FacetQueryPlanner.d.ts +33 -0
  17. package/dist/atlas/FacetQueryPlanner.js +229 -0
  18. package/dist/atlas/GraphCurator.d.ts +30 -0
  19. package/dist/atlas/GraphCurator.js +501 -0
  20. package/dist/atlas/MemoryAtlasIndexer.d.ts +15 -0
  21. package/dist/atlas/MemoryAtlasIndexer.js +55 -6
  22. package/dist/atlas/MemoryAtlasQueryCompiler.js +10 -7
  23. package/dist/atlas/MemoryAtlasService.d.ts +4 -1
  24. package/dist/atlas/MemoryAtlasService.js +174 -11
  25. package/dist/atlas/MemoryAtlasTypes.d.ts +75 -11
  26. package/dist/bin/memory.js +18 -5
  27. package/dist/factory.d.ts +18 -0
  28. package/dist/factory.js +41 -2
  29. package/dist/host/openclaw/AutoMemoryPluginInstaller.js +135 -39
  30. package/dist/mcp/server.js +1 -1
  31. package/dist/recall/RecallExplanation.d.ts +1 -1
  32. package/dist/store/MemoryAtlasStore.d.ts +12 -1
  33. package/dist/store/MemoryAtlasStore.js +353 -15
  34. package/dist/utils/ActionKindRegistry.d.ts +10 -0
  35. package/dist/utils/ActionKindRegistry.js +18 -0
  36. package/dist/utils/EntityCueExtractor.d.ts +13 -0
  37. package/dist/utils/EntityCueExtractor.js +81 -0
  38. package/examples/hermes-backend/AGENTS.md +3 -3
  39. package/examples/hermes-backend/README.md +1 -1
  40. package/examples/hermes-backend/SKILL.md +14 -5
  41. package/examples/hermes-backend/references/operations.md +12 -9
  42. package/examples/openclaw-backend/AGENTS.md +3 -2
  43. package/examples/openclaw-backend/README.md +3 -2
  44. package/examples/openclaw-backend/SKILL.md +31 -9
  45. package/examples/openclaw-backend/references/operations.md +29 -11
  46. package/package.json +1 -1
@@ -0,0 +1,501 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { eventTextForMemory } from '../episode/CogmemBlockStripper.js';
3
+ import { EpisodeTitleGenerator } from './EpisodeTitleGenerator.js';
4
+ import { extractEntityCues, normalizeEntityCueId } from '../utils/EntityCueExtractor.js';
5
+ import { inferActionKinds } from '../utils/ActionKindRegistry.js';
6
+ const FACET_EDGE_RELATIONS = new Set([
7
+ 'OCCURRED_ON',
8
+ 'OCCURRED_IN',
9
+ 'ABOUT_TOPIC',
10
+ 'PART_OF_ISSUE',
11
+ 'INVOLVES_ENTITY',
12
+ 'IN_SESSION',
13
+ 'IN_THREAD',
14
+ 'HAS_EVIDENCE',
15
+ 'HAS_MEMORY_KIND',
16
+ 'HAS_ACTION_KIND',
17
+ 'SAME_ISSUE',
18
+ 'FOLLOWS_UP',
19
+ 'REFINES',
20
+ 'CORRECTS',
21
+ 'CONTRADICTS',
22
+ 'SUPERSEDES',
23
+ 'RELATED_TO',
24
+ ]);
25
+ export class GraphCurator {
26
+ db;
27
+ eventStore;
28
+ atlasStore;
29
+ titleGenerator = new EpisodeTitleGenerator();
30
+ constructor(db, eventStore, atlasStore) {
31
+ this.db = db;
32
+ this.eventStore = eventStore;
33
+ this.atlasStore = atlasStore;
34
+ }
35
+ rebuild(projectId, now = Date.now()) {
36
+ this.deleteFacetEdges(projectId);
37
+ const rows = this.db.prepare(`
38
+ SELECT episode_id,project_id,session_id,conversation_thread_id,topic_path,episode_type,status,
39
+ importance,summary,start_event_id,end_event_id,event_count,started_at,updated_at
40
+ FROM memory_episodes
41
+ WHERE project_id=?
42
+ AND COALESCE(event_count,0)>0
43
+ AND status NOT IN ('archived','rejected','merged','invalidated')
44
+ ORDER BY started_at ASC, episode_id ASC
45
+ `).all(projectId);
46
+ const projections = [];
47
+ let facetNodeCount = 0;
48
+ let facetEdgeCount = 0;
49
+ let reviewNeeded = 0;
50
+ for (const row of rows) {
51
+ const eventIds = this.episodeEventIds(row.episode_id);
52
+ const events = eventIds.map((eventId) => this.eventStore.getEvent(eventId)).filter((event) => Boolean(event));
53
+ const title = this.titleGenerator.generate({
54
+ episodeId: row.episode_id,
55
+ summary: row.summary,
56
+ topicPath: row.topic_path,
57
+ episodeType: row.episode_type,
58
+ startedAt: row.started_at,
59
+ events,
60
+ });
61
+ const evidenceEventIds = Array.from(new Set([...title.sourceEventIds, ...eventIds])).slice(0, 30);
62
+ if (title.reviewNeeded)
63
+ reviewNeeded += 1;
64
+ this.atlasStore.upsertDocument({
65
+ id: `episode:${row.episode_id}`,
66
+ projectId,
67
+ nodeType: 'episode',
68
+ sourceId: row.episode_id,
69
+ label: title.displayTitle,
70
+ summary: title.oneLineSummary,
71
+ topicPath: row.topic_path || undefined,
72
+ confidence: Math.max(0.01, Math.min(1, Number(row.importance || 0.5))),
73
+ supportCount: Math.max(1, Number(row.event_count || evidenceEventIds.length || 1)),
74
+ status: row.status,
75
+ occurredAt: row.started_at,
76
+ evidenceEventIds,
77
+ metadata: {
78
+ originalSummary: row.summary || undefined,
79
+ displayTitle: title.displayTitle,
80
+ titleConfidence: title.confidence,
81
+ topicHints: title.topicHints,
82
+ issueHints: title.issueHints,
83
+ eventKind: title.eventKind,
84
+ userIntent: title.userIntent,
85
+ localDate: title.localDate,
86
+ reviewNeeded: title.reviewNeeded,
87
+ generatorTrace: title.generatorTrace,
88
+ episodeType: row.episode_type,
89
+ sessionId: row.session_id || undefined,
90
+ threadId: row.conversation_thread_id || undefined,
91
+ canonicalId: `episode:${row.episode_id}`,
92
+ },
93
+ updatedAt: now,
94
+ });
95
+ for (const event of events.slice(0, 30)) {
96
+ this.upsertRawEventNode(projectId, event, now);
97
+ }
98
+ const projection = {
99
+ row,
100
+ eventIds,
101
+ events,
102
+ issueHints: title.issueHints,
103
+ topicHints: title.topicHints,
104
+ localDate: title.localDate,
105
+ };
106
+ projections.push(projection);
107
+ const targets = this.facetTargetsFor(projection);
108
+ for (const target of targets) {
109
+ this.upsertFacetNode(projectId, target, projection, now);
110
+ facetNodeCount += 1;
111
+ this.upsertEdge({
112
+ projectId,
113
+ sourceType: 'episode',
114
+ sourceId: row.episode_id,
115
+ relationType: target.relation,
116
+ targetType: target.type,
117
+ targetId: target.id,
118
+ confidence: target.confidence,
119
+ evidenceEventIds,
120
+ status: 'active',
121
+ sourceAuthority: 'atlas_curator',
122
+ now,
123
+ });
124
+ facetEdgeCount += 1;
125
+ }
126
+ }
127
+ facetEdgeCount += this.projectEpisodeRelations(projectId, projections, now);
128
+ return { episodeCount: rows.length, facetNodeCount, facetEdgeCount, reviewNeeded };
129
+ }
130
+ rebuildEpisodes(projectId, episodeIds, now = Date.now()) {
131
+ const bounded = Array.from(new Set(episodeIds.filter(Boolean))).slice(0, 100);
132
+ if (!bounded.length)
133
+ return { episodeCount: 0, facetNodeCount: 0, facetEdgeCount: 0, reviewNeeded: 0 };
134
+ this.deleteFacetEdgesForEpisodes(projectId, bounded);
135
+ const rows = this.db.prepare(`
136
+ SELECT episode_id,project_id,session_id,conversation_thread_id,topic_path,episode_type,status,
137
+ importance,summary,start_event_id,end_event_id,event_count,started_at,updated_at
138
+ FROM memory_episodes
139
+ WHERE project_id=? AND episode_id IN (${bounded.map(() => '?').join(',')})
140
+ AND COALESCE(event_count,0)>0
141
+ AND status NOT IN ('archived','rejected','merged','invalidated')
142
+ ORDER BY started_at ASC, episode_id ASC
143
+ `).all(projectId, ...bounded);
144
+ let facetNodeCount = 0;
145
+ let facetEdgeCount = 0;
146
+ let reviewNeeded = 0;
147
+ for (const row of rows) {
148
+ const projection = this.projectEpisode(row, projectId, now);
149
+ facetNodeCount += projection.facetNodeCount;
150
+ facetEdgeCount += projection.facetEdgeCount;
151
+ reviewNeeded += projection.reviewNeeded;
152
+ }
153
+ return { episodeCount: rows.length, facetNodeCount, facetEdgeCount, reviewNeeded };
154
+ }
155
+ projectEpisode(row, projectId, now) {
156
+ const eventIds = this.episodeEventIds(row.episode_id);
157
+ const events = eventIds.map((eventId) => this.eventStore.getEvent(eventId)).filter((event) => Boolean(event));
158
+ const title = this.titleGenerator.generate({
159
+ episodeId: row.episode_id,
160
+ summary: row.summary,
161
+ topicPath: row.topic_path,
162
+ episodeType: row.episode_type,
163
+ startedAt: row.started_at,
164
+ events,
165
+ });
166
+ const evidenceEventIds = Array.from(new Set([...title.sourceEventIds, ...eventIds])).slice(0, 30);
167
+ this.atlasStore.upsertDocument({
168
+ id: `episode:${row.episode_id}`,
169
+ projectId,
170
+ nodeType: 'episode',
171
+ sourceId: row.episode_id,
172
+ label: title.displayTitle,
173
+ summary: title.oneLineSummary,
174
+ topicPath: row.topic_path || undefined,
175
+ confidence: Math.max(0.01, Math.min(1, Number(row.importance || 0.5))),
176
+ supportCount: Math.max(1, Number(row.event_count || evidenceEventIds.length || 1)),
177
+ status: row.status,
178
+ occurredAt: row.started_at,
179
+ evidenceEventIds,
180
+ metadata: {
181
+ originalSummary: row.summary || undefined,
182
+ displayTitle: title.displayTitle,
183
+ titleConfidence: title.confidence,
184
+ topicHints: title.topicHints,
185
+ issueHints: title.issueHints,
186
+ eventKind: title.eventKind,
187
+ userIntent: title.userIntent,
188
+ localDate: title.localDate,
189
+ reviewNeeded: title.reviewNeeded,
190
+ generatorTrace: title.generatorTrace,
191
+ episodeType: row.episode_type,
192
+ sessionId: row.session_id || undefined,
193
+ threadId: row.conversation_thread_id || undefined,
194
+ canonicalId: `episode:${row.episode_id}`,
195
+ },
196
+ updatedAt: now,
197
+ });
198
+ for (const event of events.slice(0, 30))
199
+ this.upsertRawEventNode(projectId, event, now);
200
+ const projection = { row, eventIds, events, issueHints: title.issueHints, topicHints: title.topicHints, localDate: title.localDate };
201
+ let facetNodeCount = 0;
202
+ let facetEdgeCount = 0;
203
+ for (const target of this.facetTargetsFor(projection)) {
204
+ this.upsertFacetNode(projectId, target, projection, now);
205
+ facetNodeCount += 1;
206
+ this.upsertEdge({
207
+ projectId,
208
+ sourceType: 'episode',
209
+ sourceId: row.episode_id,
210
+ relationType: target.relation,
211
+ targetType: target.type,
212
+ targetId: target.id,
213
+ confidence: target.confidence,
214
+ evidenceEventIds,
215
+ status: 'active',
216
+ sourceAuthority: 'atlas_curator',
217
+ now,
218
+ });
219
+ facetEdgeCount += 1;
220
+ }
221
+ return { projection, facetNodeCount, facetEdgeCount, reviewNeeded: title.reviewNeeded ? 1 : 0 };
222
+ }
223
+ episodeEventIds(episodeId) {
224
+ const rows = this.db.prepare(`SELECT event_id FROM memory_episode_events WHERE episode_id=? ORDER BY position ASC`).all(episodeId);
225
+ return rows.map((row) => row.event_id).filter(Boolean);
226
+ }
227
+ facetTargetsFor(projection) {
228
+ const targets = [];
229
+ const date = projection.localDate ?? dateFromTimestamp(projection.row.started_at);
230
+ if (date) {
231
+ const [year, month] = [date.slice(0, 4), date.slice(0, 7)];
232
+ targets.push({ type: 'time', id: date, nodeId: `time:${projection.row.project_id}:${date}`, label: date, relation: 'OCCURRED_ON', confidence: 1 });
233
+ targets.push({ type: 'time', id: month, nodeId: `time:${projection.row.project_id}:${month}`, label: month, relation: 'OCCURRED_IN', confidence: 1 });
234
+ targets.push({ type: 'time', id: year, nodeId: `time:${projection.row.project_id}:${year}`, label: year, relation: 'OCCURRED_IN', confidence: 1 });
235
+ }
236
+ for (const topicHint of normalizedHints(projection.topicHints)) {
237
+ const topicPath = `PROJECT/${projection.row.project_id}/${topicHint}`;
238
+ targets.push({ type: 'topic', id: topicPath, nodeId: `topic:${projection.row.project_id}:${topicPath}`, label: topicLabel(topicHint), relation: 'ABOUT_TOPIC', confidence: 0.86 });
239
+ }
240
+ if (projection.row.topic_path) {
241
+ targets.push({ type: 'topic', id: projection.row.topic_path, nodeId: `topic:${projection.row.project_id}:${projection.row.topic_path}`, label: projection.row.topic_path, relation: 'ABOUT_TOPIC', confidence: 0.72 });
242
+ }
243
+ for (const issueHint of normalizedHints(projection.issueHints)) {
244
+ targets.push({ type: 'issue', id: issueHint, nodeId: `issue:${projection.row.project_id}:${issueHint}`, label: issueLabel(issueHint), relation: 'PART_OF_ISSUE', confidence: 0.9 });
245
+ }
246
+ for (const entity of this.entityHintsFor(projection)) {
247
+ const entityId = normalizeEntityCueId(entity);
248
+ if (!entityId)
249
+ continue;
250
+ targets.push({ type: 'entity', id: `facet:${entityId}`, nodeId: `entity:${projection.row.project_id}:facet:${entityId}`, label: entity, relation: 'INVOLVES_ENTITY', confidence: 0.78 });
251
+ }
252
+ if (projection.row.session_id) {
253
+ targets.push({ type: 'session', id: projection.row.session_id, nodeId: `session:${projection.row.project_id}:${projection.row.session_id}`, label: `Session ${projection.row.session_id}`, relation: 'IN_SESSION', confidence: 1 });
254
+ }
255
+ if (projection.row.conversation_thread_id) {
256
+ targets.push({ type: 'thread', id: projection.row.conversation_thread_id, nodeId: `thread:${projection.row.project_id}:${projection.row.conversation_thread_id}`, label: `Thread ${projection.row.conversation_thread_id}`, relation: 'IN_THREAD', confidence: 1 });
257
+ }
258
+ const memoryKind = normalizeKind(projection.issueHints[0] ? issueKind(projection.issueHints[0]) : projection.row.episode_type || 'discussion');
259
+ targets.push({ type: 'memoryKind', id: memoryKind, nodeId: `memoryKind:${projection.row.project_id}:${memoryKind}`, label: memoryKind, relation: 'HAS_MEMORY_KIND', confidence: 0.75 });
260
+ for (const actionKind of actionKindsFor(projection)) {
261
+ targets.push({ type: 'actionKind', id: actionKind, nodeId: `actionKind:${projection.row.project_id}:${actionKind}`, label: actionKind, relation: 'HAS_ACTION_KIND', confidence: 0.7 });
262
+ }
263
+ for (const eventId of projection.eventIds.slice(0, 30)) {
264
+ targets.push({ type: 'raw_event', id: eventId, nodeId: `raw_event:${eventId}`, label: eventId, relation: 'HAS_EVIDENCE', confidence: 1 });
265
+ }
266
+ return dedupeTargets(targets);
267
+ }
268
+ upsertFacetNode(projectId, target, projection, now) {
269
+ if (target.type === 'raw_event')
270
+ return;
271
+ this.atlasStore.upsertDocument({
272
+ id: target.nodeId,
273
+ projectId,
274
+ nodeType: target.type,
275
+ sourceId: target.id,
276
+ label: target.label,
277
+ summary: facetSummary(target),
278
+ topicPath: target.type === 'topic' ? target.id : undefined,
279
+ confidence: target.confidence,
280
+ supportCount: 1,
281
+ status: 'active',
282
+ occurredAt: target.type === 'time' ? projection.row.started_at : undefined,
283
+ evidenceEventIds: projection.eventIds.slice(0, 20),
284
+ metadata: {
285
+ projection: 'memory_atlas.facets.v1',
286
+ facetType: target.type,
287
+ facetValue: target.id,
288
+ },
289
+ updatedAt: now,
290
+ });
291
+ }
292
+ upsertRawEventNode(projectId, event, now) {
293
+ const text = eventTextForMemory(event);
294
+ this.atlasStore.upsertDocument({
295
+ id: `raw_event:${event.eventId}`,
296
+ projectId,
297
+ nodeType: 'raw_event',
298
+ sourceId: event.eventId,
299
+ label: `${event.role || 'event'} ${new Date(event.occurredAt).toISOString().slice(0, 10)}`,
300
+ summary: text.slice(0, 220),
301
+ confidence: 1,
302
+ supportCount: 1,
303
+ status: 'active',
304
+ occurredAt: event.occurredAt,
305
+ evidenceEventIds: [event.eventId],
306
+ metadata: { projection: 'memory_atlas.facets.v1', role: event.role, localDate: event.localDate },
307
+ updatedAt: now,
308
+ });
309
+ }
310
+ projectEpisodeRelations(projectId, projections, now) {
311
+ let count = 0;
312
+ const seen = new Set();
313
+ for (const bucket of bucketByHint(projections, (projection) => projection.issueHints)) {
314
+ const sorted = bucket.slice().sort((left, right) => left.row.started_at - right.row.started_at);
315
+ for (let index = 1; index < sorted.length; index += 1) {
316
+ const older = sorted[index - 1];
317
+ const newer = sorted[index];
318
+ const sameIssueKey = relationKey(older, newer, 'SAME_ISSUE');
319
+ if (!seen.has(sameIssueKey)) {
320
+ this.upsertEpisodeRelation(projectId, older, newer, 'SAME_ISSUE', 0.8, now);
321
+ seen.add(sameIssueKey);
322
+ count += 1;
323
+ }
324
+ const followsKey = relationKey(newer, older, 'FOLLOWS_UP');
325
+ if (!seen.has(followsKey)) {
326
+ this.upsertEpisodeRelation(projectId, newer, older, 'FOLLOWS_UP', 0.76, now);
327
+ seen.add(followsKey);
328
+ count += 1;
329
+ }
330
+ }
331
+ }
332
+ for (const bucket of bucketByHint(projections, (projection) => projection.topicHints)) {
333
+ const sorted = bucket.slice().sort((left, right) => left.row.started_at - right.row.started_at);
334
+ for (let index = 1; index < sorted.length; index += 1) {
335
+ const older = sorted[index - 1];
336
+ const newer = sorted[index];
337
+ if (intersection(older.issueHints, newer.issueHints).length)
338
+ continue;
339
+ const key = relationKey(older, newer, 'RELATED_TO');
340
+ if (seen.has(key))
341
+ continue;
342
+ this.upsertEpisodeRelation(projectId, older, newer, 'RELATED_TO', 0.45, now, 'weak');
343
+ seen.add(key);
344
+ count += 1;
345
+ }
346
+ }
347
+ return count;
348
+ }
349
+ upsertEpisodeRelation(projectId, left, right, relationType, confidence, now, status = 'active') {
350
+ this.upsertEdge({
351
+ projectId,
352
+ sourceType: 'episode',
353
+ sourceId: left.row.episode_id,
354
+ relationType,
355
+ targetType: 'episode',
356
+ targetId: right.row.episode_id,
357
+ confidence,
358
+ evidenceEventIds: Array.from(new Set([...left.eventIds.slice(0, 3), ...right.eventIds.slice(0, 3)])),
359
+ status,
360
+ sourceAuthority: 'atlas_curator',
361
+ now,
362
+ });
363
+ }
364
+ entityHintsFor(projection) {
365
+ const hints = new Set(entityHints(projection.events));
366
+ if (projection.eventIds.length) {
367
+ const rows = this.db.prepare(`
368
+ SELECT entity_name FROM memory_bindings
369
+ WHERE project_id=? AND event_id IN (${projection.eventIds.map(() => '?').join(',')})
370
+ AND COALESCE(entity_name,'')<>''
371
+ ORDER BY confidence DESC, created_at DESC LIMIT 20
372
+ `).all(projection.row.project_id, ...projection.eventIds);
373
+ for (const row of rows)
374
+ if (row.entity_name?.trim())
375
+ hints.add(row.entity_name.trim());
376
+ }
377
+ return Array.from(hints);
378
+ }
379
+ deleteFacetEdges(projectId) {
380
+ const relations = Array.from(FACET_EDGE_RELATIONS);
381
+ this.db.prepare(`DELETE FROM memory_edges WHERE project_id=? AND source_authority='atlas_curator' AND relation_type IN (${relations.map(() => '?').join(',')})`).run(projectId, ...relations);
382
+ }
383
+ deleteFacetEdgesForEpisodes(projectId, episodeIds) {
384
+ const relations = Array.from(FACET_EDGE_RELATIONS);
385
+ this.db.prepare(`
386
+ DELETE FROM memory_edges
387
+ WHERE project_id=? AND source_authority='atlas_curator'
388
+ AND relation_type IN (${relations.map(() => '?').join(',')})
389
+ AND (
390
+ (source_type='episode' AND source_id IN (${episodeIds.map(() => '?').join(',')}))
391
+ OR (target_type='episode' AND target_id IN (${episodeIds.map(() => '?').join(',')}))
392
+ )
393
+ `).run(projectId, ...relations, ...episodeIds, ...episodeIds);
394
+ }
395
+ upsertEdge(input) {
396
+ const edgeId = createHash('sha256')
397
+ .update([input.projectId, input.sourceType, input.sourceId, input.relationType, input.targetType, input.targetId].join('\0'))
398
+ .digest('hex');
399
+ this.db.prepare(`
400
+ INSERT INTO memory_edges (
401
+ edge_id, project_id, source_type, source_id, relation_type, target_type, target_id,
402
+ confidence, base_weight, stability, activation, evidence_event_ids_json, status,
403
+ valid_from, valid_to, version, source_authority, created_at, updated_at
404
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
405
+ ON CONFLICT(edge_id) DO UPDATE SET
406
+ confidence=excluded.confidence,
407
+ evidence_event_ids_json=excluded.evidence_event_ids_json,
408
+ status=excluded.status,
409
+ source_authority=excluded.source_authority,
410
+ updated_at=excluded.updated_at
411
+ `).run(edgeId, input.projectId, input.sourceType, input.sourceId, input.relationType, input.targetType, input.targetId, input.confidence, 1, input.status === 'weak' ? 0.35 : 0.85, 1, JSON.stringify(Array.from(new Set(input.evidenceEventIds)).slice(0, 30)), input.status, input.now, null, 1, input.sourceAuthority, input.now, input.now);
412
+ }
413
+ }
414
+ function dateFromTimestamp(timestamp) {
415
+ if (!Number.isFinite(timestamp))
416
+ return undefined;
417
+ return new Date(timestamp).toISOString().slice(0, 10);
418
+ }
419
+ function normalizedHints(values) {
420
+ return Array.from(new Set(values.map((value) => normalizeKind(value)).filter(Boolean)));
421
+ }
422
+ function normalizeKind(value) {
423
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'general';
424
+ }
425
+ function topicLabel(value) {
426
+ if (value === 'memory-blackbox')
427
+ return '记忆黑盒';
428
+ if (value === 'source-drilldown')
429
+ return '原文下钻';
430
+ if (value === 'context-injection')
431
+ return '上下文注入';
432
+ if (value === 'atlas-readability')
433
+ return 'Atlas 可读性';
434
+ return value;
435
+ }
436
+ function issueLabel(value) {
437
+ if (value === 'memory-context-blackbox')
438
+ return 'Memory Context 黑盒';
439
+ if (value === 'graph-runtime-blackbox')
440
+ return 'Graph Runtime 黑盒';
441
+ if (value === 'atlas-readability')
442
+ return 'Atlas 可读性黑盒';
443
+ if (value === 'auto-injection-mismatch')
444
+ return '自动注入不一致';
445
+ return value;
446
+ }
447
+ function issueKind(value) {
448
+ if (value.includes('graph-runtime'))
449
+ return 'bug';
450
+ if (value.includes('atlas'))
451
+ return 'plan';
452
+ return 'diagnostic';
453
+ }
454
+ function actionKindsFor(projection) {
455
+ const text = projection.events.map(eventTextForMemory).join('\n');
456
+ return inferActionKinds(text);
457
+ }
458
+ function entityHints(events) {
459
+ const text = events.map(eventTextForMemory).join('\n');
460
+ return extractEntityCues(text).map((entity) => entity.label);
461
+ }
462
+ function facetSummary(target) {
463
+ if (target.type === 'topic')
464
+ return `Topic facet for ${target.label}.`;
465
+ if (target.type === 'issue')
466
+ return `Issue facet for ${target.label}.`;
467
+ if (target.type === 'time')
468
+ return `Time facet for ${target.label}.`;
469
+ return `${target.type} facet for ${target.label}.`;
470
+ }
471
+ function dedupeTargets(targets) {
472
+ const seen = new Set();
473
+ return targets.filter((target) => {
474
+ const key = `${target.type}:${target.id}:${target.relation}`;
475
+ if (seen.has(key))
476
+ return false;
477
+ seen.add(key);
478
+ return true;
479
+ });
480
+ }
481
+ function intersection(left, right) {
482
+ const rightSet = new Set(right);
483
+ return left.filter((value) => rightSet.has(value));
484
+ }
485
+ function bucketByHint(projections, hintsFor) {
486
+ const buckets = new Map();
487
+ for (const projection of projections) {
488
+ for (const hint of normalizedHints(hintsFor(projection))) {
489
+ const bucket = buckets.get(hint) ?? [];
490
+ bucket.push(projection);
491
+ buckets.set(hint, bucket);
492
+ }
493
+ }
494
+ return Array.from(buckets.values()).filter((bucket) => bucket.length > 1);
495
+ }
496
+ function relationKey(left, right, relationType) {
497
+ const pair = relationType === 'SAME_ISSUE' || relationType === 'RELATED_TO'
498
+ ? [left.row.episode_id, right.row.episode_id].sort().join('\0')
499
+ : `${left.row.episode_id}\0${right.row.episode_id}`;
500
+ return `${relationType}\0${pair}`;
501
+ }
@@ -5,20 +5,35 @@ export declare class MemoryAtlasIndexer {
5
5
  private db;
6
6
  private store;
7
7
  private readonly actions;
8
+ private readonly curator;
8
9
  constructor(db: Database, eventStore: EventStore, store: MemoryAtlasStore);
9
10
  rebuild(options?: {
10
11
  projectId?: string;
11
12
  }): {
12
13
  documents: number;
13
14
  actions: number;
15
+ curatedEpisodes: number;
14
16
  };
15
17
  ensureFresh(options: {
16
18
  projectId: string;
17
19
  }): {
18
20
  documents: number;
19
21
  actions: number;
22
+ curatedEpisodes?: number;
20
23
  refreshed: boolean;
21
24
  };
25
+ reindex(options: {
26
+ projectId: string;
27
+ eventId?: string;
28
+ episodeId?: string;
29
+ }): {
30
+ projectId: string;
31
+ episodeIds: string[];
32
+ refreshed: boolean;
33
+ curatedEpisodes: number;
34
+ facetEdges: number;
35
+ reviewNeeded: number;
36
+ };
22
37
  ensureAllFresh(): {
23
38
  documents: number;
24
39
  actions: number;
@@ -1,25 +1,32 @@
1
+ import { MEMORY_ATLAS_PROJECTION_NAME, MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION } from '../store/MemoryAtlasStore.js';
1
2
  import { backfillAtlasDocuments, installAtlasProjectionDirtyTriggers } from '../migrations/0025_memory_atlas.js';
2
3
  import { ActionFrameExtractor } from './ActionFrameExtractor.js';
4
+ import { GraphCurator } from './GraphCurator.js';
3
5
  export class MemoryAtlasIndexer {
4
6
  db;
5
7
  store;
6
8
  actions;
9
+ curator;
7
10
  constructor(db, eventStore, store) {
8
11
  this.db = db;
9
12
  this.store = store;
10
13
  installAtlasProjectionDirtyTriggers(db);
11
14
  this.actions = new ActionFrameExtractor(db, eventStore, store);
15
+ this.curator = new GraphCurator(db, eventStore, store);
12
16
  }
13
17
  rebuild(options = {}) {
14
18
  const projectId = options.projectId;
15
19
  let actions = 0;
20
+ let curatedEpisodes = 0;
21
+ let facetEdges = 0;
22
+ let reviewNeeded = 0;
16
23
  try {
17
24
  this.db.transaction(() => {
18
25
  if (projectId) {
19
- this.db.prepare(`DELETE FROM memory_atlas_documents WHERE project_id=? AND node_type IN ('project','entity','topic','cluster','episode','belief')`).run(projectId);
26
+ this.db.prepare(`DELETE FROM memory_atlas_documents WHERE project_id=? AND node_type IN ('project','entity','topic','issue','session','thread','memoryKind','actionKind','cluster','episode','raw_event','belief','time')`).run(projectId);
20
27
  }
21
28
  else {
22
- this.db.exec(`DELETE FROM memory_atlas_documents WHERE node_type IN ('project','entity','topic','cluster','episode','belief');`);
29
+ this.db.exec(`DELETE FROM memory_atlas_documents WHERE node_type IN ('project','entity','topic','issue','session','thread','memoryKind','actionKind','cluster','episode','raw_event','belief','time');`);
23
30
  }
24
31
  backfillAtlasDocuments(this.db, projectId);
25
32
  const projects = projectId
@@ -29,15 +36,22 @@ export class MemoryAtlasIndexer {
29
36
  this.store.upsertDocument({
30
37
  id: `project:${id}`, projectId: id, nodeType: 'project', sourceId: id, label: id,
31
38
  confidence: 1, supportCount: this.store.countDocuments(id), status: 'active', evidenceEventIds: [],
32
- metadata: { projection: 'memory_atlas.v1' },
39
+ metadata: { projection: MEMORY_ATLAS_PROJECTION_NAME, projectionSchemaVersion: MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION },
33
40
  });
34
41
  actions = this.actions.rebuild(projectId);
42
+ for (const id of projects) {
43
+ const result = this.curator.rebuild(id);
44
+ curatedEpisodes += result.episodeCount;
45
+ facetEdges += result.facetEdgeCount;
46
+ reviewNeeded += result.reviewNeeded;
47
+ this.store.aggregateFacetNodeSupport(id);
48
+ }
35
49
  if (projectId) {
36
- this.store.markProjectionClean(projectId, { actions });
50
+ this.store.markProjectionClean(projectId, { actions, curatedEpisodes, facetEdges, reviewNeeded });
37
51
  }
38
52
  else {
39
53
  for (const id of projects)
40
- this.store.markProjectionClean(id, { actions });
54
+ this.store.markProjectionClean(id, { actions, curatedEpisodes, facetEdges, reviewNeeded });
41
55
  }
42
56
  })();
43
57
  }
@@ -45,7 +59,7 @@ export class MemoryAtlasIndexer {
45
59
  this.store.markProjectionFailed(projectId || '__global__', error instanceof Error ? error.message : String(error));
46
60
  throw error;
47
61
  }
48
- return { documents: this.store.countDocuments(projectId), actions };
62
+ return { documents: this.store.countDocuments(projectId), actions, curatedEpisodes };
49
63
  }
50
64
  ensureFresh(options) {
51
65
  if (!this.store.projectionNeedsRefresh(options.projectId)) {
@@ -53,6 +67,41 @@ export class MemoryAtlasIndexer {
53
67
  }
54
68
  return { ...this.rebuild(options), refreshed: true };
55
69
  }
70
+ reindex(options) {
71
+ const episodeIds = new Set();
72
+ if (options.episodeId)
73
+ episodeIds.add(options.episodeId);
74
+ if (options.eventId) {
75
+ const rows = this.db.prepare(`SELECT episode_id FROM memory_episode_events WHERE event_id=?`).all(options.eventId);
76
+ for (const row of rows)
77
+ if (row.episode_id)
78
+ episodeIds.add(row.episode_id);
79
+ }
80
+ const ids = Array.from(episodeIds);
81
+ if (!ids.length)
82
+ throw new Error('graph_reindex_target_not_found');
83
+ let result = { episodeCount: 0, facetNodeCount: 0, facetEdgeCount: 0, reviewNeeded: 0 };
84
+ this.db.transaction(() => {
85
+ result = this.curator.rebuildEpisodes(options.projectId, ids);
86
+ this.store.markProjectionDirty(options.projectId, {
87
+ targetedReindex: true,
88
+ episodeIds: ids,
89
+ eventId: options.eventId,
90
+ curatedEpisodes: result.episodeCount,
91
+ facetEdges: result.facetEdgeCount,
92
+ reviewNeeded: result.reviewNeeded,
93
+ reason: 'targeted_reindex_requires_full_consistency_rebuild',
94
+ });
95
+ })();
96
+ return {
97
+ projectId: options.projectId,
98
+ episodeIds: ids,
99
+ refreshed: true,
100
+ curatedEpisodes: result.episodeCount,
101
+ facetEdges: result.facetEdgeCount,
102
+ reviewNeeded: result.reviewNeeded,
103
+ };
104
+ }
56
105
  ensureAllFresh() {
57
106
  let documents = 0;
58
107
  let actions = 0;
@@ -1,5 +1,5 @@
1
1
  const STOP_WORDS = new Set(['我', '你', '让', '对', '的', '年', '做过', '什么', '去年', '今年', 'the', 'a', 'an', 'what', 'did', 'do', 'to', 'last', 'year']);
2
- const ACTION_MARKERS = /配置|连接|安装|修复|更新|升级|比较|操作|设置|调试|configure|connect|install|repair|fix|update|upgrade|compare|setup|debug/iu;
2
+ const ACTION_MARKERS = /启动|重启|停止|执行|配置|连接|安装|修复|更新|升级|比较|操作|设置|调试|start|started|launch|launched|boot|restart|restarted|stop|stopped|run|ran|configure|connect|install|repair|fix|update|upgrade|compare|setup|debug/iu;
3
3
  const MEMORY_KIND_MARKERS = [
4
4
  [/决策|决定|decision/iu, 'decision'], [/修正|纠正|correction/iu, 'correction'],
5
5
  [/目标|goal/iu, 'goal'], [/偏好|preference/iu, 'preference'], [/计划|plan/iu, 'plan'],
@@ -45,11 +45,14 @@ export function actionMarkers(value) {
45
45
  }
46
46
  function frameTypeForAction(lower) {
47
47
  const frameType = /修复|fix|repair|调试|debug/u.test(lower) ? 'repair'
48
- : /安装|install/u.test(lower) ? 'install'
49
- : /连接|connect/u.test(lower) ? 'connect'
50
- : /更新|升级|update|upgrade/u.test(lower) ? 'update'
51
- : /比较|compare/u.test(lower) ? 'compare'
52
- : /配置|设置|configure|setup/u.test(lower) ? 'configuration'
53
- : 'operation';
48
+ : /启动|start|launch|boot/u.test(lower) ? 'start'
49
+ : /重启|restart/u.test(lower) ? 'restart'
50
+ : /停止|stop/u.test(lower) ? 'stop'
51
+ : /安装|install/u.test(lower) ? 'install'
52
+ : /连接|connect/u.test(lower) ? 'connect'
53
+ : /更新|升级|update|upgrade/u.test(lower) ? 'update'
54
+ : /比较|compare/u.test(lower) ? 'compare'
55
+ : /配置|设置|configure|setup/u.test(lower) ? 'configuration'
56
+ : 'operation';
54
57
  return frameType;
55
58
  }