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.
- package/CHANGELOG.md +24 -0
- package/MEMORY_ATLAS.md +59 -12
- package/README.md +21 -13
- package/RELEASE_CHECKLIST.md +5 -4
- package/dist/agent/AgentMemoryBackend.d.ts +24 -3
- package/dist/agent/AgentMemoryBackend.js +285 -55
- package/dist/agent/AgentRecallQueryCompiler.d.ts +1 -1
- package/dist/agent/AgentRecallQueryCompiler.js +13 -1
- package/dist/agent/MemoryUsageReceipt.js +8 -10
- package/dist/agent/SessionWorkingState.js +3 -2
- package/dist/agent/UntrustedMemorySerializer.d.ts +2 -0
- package/dist/agent/UntrustedMemorySerializer.js +14 -0
- package/dist/atlas/ActionFrameExtractor.js +2 -0
- package/dist/atlas/EpisodeTitleGenerator.d.ts +31 -0
- package/dist/atlas/EpisodeTitleGenerator.js +191 -0
- package/dist/atlas/FacetQueryPlanner.d.ts +33 -0
- package/dist/atlas/FacetQueryPlanner.js +229 -0
- package/dist/atlas/GraphCurator.d.ts +30 -0
- package/dist/atlas/GraphCurator.js +501 -0
- package/dist/atlas/MemoryAtlasIndexer.d.ts +15 -0
- package/dist/atlas/MemoryAtlasIndexer.js +55 -6
- package/dist/atlas/MemoryAtlasQueryCompiler.js +10 -7
- package/dist/atlas/MemoryAtlasService.d.ts +4 -1
- package/dist/atlas/MemoryAtlasService.js +174 -11
- package/dist/atlas/MemoryAtlasTypes.d.ts +75 -11
- package/dist/bin/memory.js +18 -5
- package/dist/factory.d.ts +18 -0
- package/dist/factory.js +41 -2
- package/dist/host/openclaw/AutoMemoryPluginInstaller.js +135 -39
- package/dist/mcp/server.js +1 -1
- package/dist/recall/RecallExplanation.d.ts +1 -1
- package/dist/store/MemoryAtlasStore.d.ts +12 -1
- package/dist/store/MemoryAtlasStore.js +353 -15
- package/dist/utils/ActionKindRegistry.d.ts +10 -0
- package/dist/utils/ActionKindRegistry.js +18 -0
- package/dist/utils/EntityCueExtractor.d.ts +13 -0
- package/dist/utils/EntityCueExtractor.js +81 -0
- package/examples/hermes-backend/AGENTS.md +3 -3
- package/examples/hermes-backend/README.md +1 -1
- package/examples/hermes-backend/SKILL.md +14 -5
- package/examples/hermes-backend/references/operations.md +12 -9
- package/examples/openclaw-backend/AGENTS.md +3 -2
- package/examples/openclaw-backend/README.md +3 -2
- package/examples/openclaw-backend/SKILL.md +31 -9
- package/examples/openclaw-backend/references/operations.md +29 -11
- 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:
|
|
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 =
|
|
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
|
-
:
|
|
49
|
-
:
|
|
50
|
-
:
|
|
51
|
-
:
|
|
52
|
-
:
|
|
53
|
-
: '
|
|
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
|
}
|