cogmem 3.7.0 → 3.7.2
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 +18 -0
- package/MEMORY_ATLAS.md +16 -6
- package/README.md +16 -11
- package/RELEASE_CHECKLIST.md +4 -4
- package/dist/agent/AgentMemoryBackend.d.ts +11 -2
- package/dist/agent/AgentMemoryBackend.js +185 -36
- 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.js +42 -3
- package/dist/atlas/FacetQueryPlanner.d.ts +2 -0
- package/dist/atlas/FacetQueryPlanner.js +57 -16
- package/dist/atlas/GraphCurator.d.ts +3 -0
- package/dist/atlas/GraphCurator.js +118 -14
- package/dist/atlas/MemoryAtlasIndexer.d.ts +12 -0
- package/dist/atlas/MemoryAtlasIndexer.js +36 -0
- package/dist/atlas/MemoryAtlasQueryCompiler.js +10 -7
- package/dist/atlas/MemoryAtlasService.d.ts +1 -0
- package/dist/atlas/MemoryAtlasService.js +102 -26
- package/dist/atlas/MemoryAtlasTypes.d.ts +7 -0
- package/dist/bin/memory.js +14 -4
- package/dist/factory.d.ts +18 -0
- package/dist/factory.js +41 -2
- package/dist/host/openclaw/AutoMemoryPluginInstaller.js +71 -44
- package/dist/mcp/server.js +1 -1
- package/dist/recall/RecallExplanation.d.ts +1 -1
- package/dist/store/MemoryAtlasStore.d.ts +4 -1
- package/dist/store/MemoryAtlasStore.js +203 -25
- 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 +1 -1
- package/examples/hermes-backend/README.md +1 -1
- package/examples/hermes-backend/SKILL.md +10 -3
- package/examples/hermes-backend/references/operations.md +9 -8
- package/examples/openclaw-backend/AGENTS.md +1 -1
- package/examples/openclaw-backend/README.md +2 -2
- package/examples/openclaw-backend/SKILL.md +24 -7
- package/examples/openclaw-backend/references/operations.md +26 -10
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { extractEntityCues } from '../utils/EntityCueExtractor.js';
|
|
2
|
+
import { ACTION_KIND_RULES } from '../utils/ActionKindRegistry.js';
|
|
1
3
|
const ISSUE_RULES = [
|
|
2
4
|
{
|
|
3
5
|
value: 'memory-context-blackbox',
|
|
@@ -29,7 +31,7 @@ const TOPIC_RULES = [
|
|
|
29
31
|
{
|
|
30
32
|
value: 'source-drilldown',
|
|
31
33
|
label: '原文下钻',
|
|
32
|
-
test: (text) => /(sourcecontext|source context
|
|
34
|
+
test: (text) => /(sourcecontext|source context|原文下钻|摘要.*原文)/i.test(text),
|
|
33
35
|
},
|
|
34
36
|
{
|
|
35
37
|
value: 'context-injection',
|
|
@@ -41,7 +43,8 @@ export class FacetQueryPlanner {
|
|
|
41
43
|
plan(query, options) {
|
|
42
44
|
const normalized = query.trim();
|
|
43
45
|
const facets = [];
|
|
44
|
-
const
|
|
46
|
+
const actionHistory = isActionHistoryQuery(normalized);
|
|
47
|
+
const timeFacet = parseTimeFacet(normalized, options);
|
|
45
48
|
if (timeFacet)
|
|
46
49
|
facets.push(withNodeId(timeFacet, options.projectId));
|
|
47
50
|
const timeline = /(后来|继续|timeline|演化|发展|之后)/i.test(normalized);
|
|
@@ -60,15 +63,23 @@ export class FacetQueryPlanner {
|
|
|
60
63
|
if (!timeline && isBroadMemoryBlackbox(normalized) && matchedIssueValues.size === 0) {
|
|
61
64
|
facets.push(withNodeId({ type: 'issue', value: 'memory-context-blackbox', label: 'Memory Context 黑盒', relation: 'PART_OF_ISSUE' }, options.projectId));
|
|
62
65
|
}
|
|
63
|
-
for (const entity of
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
+
for (const entity of extractEntityCues(normalized)) {
|
|
67
|
+
if (!actionHistory && isConceptFacetEntity(entity, facets))
|
|
68
|
+
continue;
|
|
69
|
+
facets.push({ type: 'entity', value: `facet:${entity.id}`, label: entity.label, nodeId: `entity:${options.projectId}:facet:${entity.id}`, relation: 'INVOLVES_ENTITY' });
|
|
70
|
+
if (actionHistory) {
|
|
71
|
+
facets.push(withNodeId({ type: 'topic', value: `PROJECT/${options.projectId}/${entity.id}`, label: entity.label, relation: 'ABOUT_TOPIC' }, options.projectId));
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
for (const facet of parseKindFacets(normalized, options.projectId))
|
|
69
75
|
facets.push(facet);
|
|
76
|
+
if (actionHistory && (!facets.some((facet) => facet.type === 'actionKind') || !hasSpecificActionCue(normalized))) {
|
|
77
|
+
for (const value of ['started', 'installed', 'configured', 'restarted', 'stopped', 'operated', 'implemented', 'debugged']) {
|
|
78
|
+
facets.push(withNodeId({ type: 'actionKind', value, label: value, relation: 'HAS_ACTION_KIND' }, options.projectId));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
70
81
|
return {
|
|
71
|
-
intent: /聊过|记得|还记得|讨论|原话|那次/i.test(normalized) ? 'historical_discussion' : 'graph_search',
|
|
82
|
+
intent: actionHistory ? 'action_history' : /聊过|记得|还记得|讨论|原话|那次/i.test(normalized) ? 'historical_discussion' : 'graph_search',
|
|
72
83
|
operator: 'intersection',
|
|
73
84
|
facets: dedupeFacets(facets),
|
|
74
85
|
temporalIntent: timeline ? 'timeline' : undefined,
|
|
@@ -80,10 +91,26 @@ export class FacetQueryPlanner {
|
|
|
80
91
|
};
|
|
81
92
|
}
|
|
82
93
|
}
|
|
94
|
+
function isActionHistoryQuery(query) {
|
|
95
|
+
return /(让你.{0,20}(对|给|把)?.{0,20}(做过|做了|启动|安装|配置|修改|重启|停止|操作|处理|执行)|对.{0,30}(做过什么|做了什么|哪些操作)|what did (i ask you to do|you do) to|operations? on)/i.test(query);
|
|
96
|
+
}
|
|
97
|
+
function hasSpecificActionCue(query) {
|
|
98
|
+
return /(启动|start|started|launch|launched|boot|安装|install|installed|setup|配置|config|configured|设置|修改配置|重启|restart|停止|stop|修复|实现|implemented|review|审查|debug|排查|卡死|locked|zombie)/i.test(query);
|
|
99
|
+
}
|
|
83
100
|
function isBroadMemoryBlackbox(query) {
|
|
84
101
|
return /(记忆黑盒|memory.*blackbox)/i.test(query) &&
|
|
85
102
|
!/(memory graph|graph|database locked|sqlite|zombie|僵尸|卡死|atlas|图谱|节点|事件名称|自动注入|before_prompt_build|manual recall|手动.*recall)/i.test(query);
|
|
86
103
|
}
|
|
104
|
+
function isConceptFacetEntity(entity, facets) {
|
|
105
|
+
const label = entity.label.toLocaleLowerCase();
|
|
106
|
+
return facets.some((facet) => {
|
|
107
|
+
if (facet.type !== 'topic' && facet.type !== 'issue')
|
|
108
|
+
return false;
|
|
109
|
+
const facetLabel = facet.label.toLocaleLowerCase();
|
|
110
|
+
const facetSlug = facet.value.split('/').pop()?.toLocaleLowerCase();
|
|
111
|
+
return label === facetLabel || label.includes(facetLabel) || Boolean(facetSlug && entity.id === facetSlug);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
87
114
|
function parseKindFacets(query, projectId) {
|
|
88
115
|
const facets = [];
|
|
89
116
|
const memoryKindRules = [
|
|
@@ -91,23 +118,18 @@ function parseKindFacets(query, projectId) {
|
|
|
91
118
|
['decision', /(decision|决定|方案|结论)/i],
|
|
92
119
|
['plan', /(计划|下一步|策略|plan)/i],
|
|
93
120
|
];
|
|
94
|
-
const actionKindRules = [
|
|
95
|
-
['implemented', /(修复|实现|implemented|合并|发布|升级)/i],
|
|
96
|
-
['reviewed', /(检查|审查|review)/i],
|
|
97
|
-
['debugged', /(debug|排查|卡死|locked|zombie)/i],
|
|
98
|
-
];
|
|
99
121
|
for (const [value, test] of memoryKindRules) {
|
|
100
122
|
if (test.test(query))
|
|
101
123
|
facets.push(withNodeId({ type: 'memoryKind', value, label: value, relation: 'HAS_MEMORY_KIND' }, projectId));
|
|
102
124
|
}
|
|
103
|
-
for (const
|
|
104
|
-
if (
|
|
105
|
-
facets.push(withNodeId({ type: 'actionKind', value, label:
|
|
125
|
+
for (const rule of ACTION_KIND_RULES) {
|
|
126
|
+
if (rule.pattern.test(query))
|
|
127
|
+
facets.push(withNodeId({ type: 'actionKind', value: rule.kind, label: rule.kind, relation: 'HAS_ACTION_KIND' }, projectId));
|
|
106
128
|
}
|
|
107
129
|
return facets;
|
|
108
130
|
}
|
|
109
|
-
function parseTimeFacet(query,
|
|
110
|
-
const currentYear =
|
|
131
|
+
function parseTimeFacet(query, options) {
|
|
132
|
+
const currentYear = localYear(options);
|
|
111
133
|
const isoDay = query.match(/(20\d{2})[-年\/.](\d{1,2})[-月\/.](\d{1,2})日?/);
|
|
112
134
|
if (isoDay)
|
|
113
135
|
return dayFacet(Number(isoDay[1]), Number(isoDay[2]), Number(isoDay[3]));
|
|
@@ -124,6 +146,25 @@ function parseTimeFacet(query, now) {
|
|
|
124
146
|
return yearFacet(currentYear - 1);
|
|
125
147
|
return undefined;
|
|
126
148
|
}
|
|
149
|
+
function localYear(options) {
|
|
150
|
+
const explicit = options.localDateNow?.match(/^(20\d{2})-\d{2}-\d{2}$/u)?.[1];
|
|
151
|
+
if (explicit)
|
|
152
|
+
return Number(explicit);
|
|
153
|
+
const now = options.now ?? Date.now();
|
|
154
|
+
try {
|
|
155
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
156
|
+
timeZone: options.timeZone || 'Asia/Tokyo',
|
|
157
|
+
year: 'numeric',
|
|
158
|
+
month: '2-digit',
|
|
159
|
+
day: '2-digit',
|
|
160
|
+
}).formatToParts(new Date(now));
|
|
161
|
+
const year = parts.find((part) => part.type === 'year')?.value;
|
|
162
|
+
if (year)
|
|
163
|
+
return Number(year);
|
|
164
|
+
}
|
|
165
|
+
catch { /* fall back below */ }
|
|
166
|
+
return new Date(now).getUTCFullYear();
|
|
167
|
+
}
|
|
127
168
|
function dayFacet(year, month, day) {
|
|
128
169
|
const label = `${year}-${pad(month)}-${pad(day)}`;
|
|
129
170
|
return {
|
|
@@ -14,6 +14,8 @@ export declare class GraphCurator {
|
|
|
14
14
|
private readonly titleGenerator;
|
|
15
15
|
constructor(db: Database, eventStore: EventStore, atlasStore: MemoryAtlasStore);
|
|
16
16
|
rebuild(projectId: string, now?: number): GraphCuratorResult;
|
|
17
|
+
rebuildEpisodes(projectId: string, episodeIds: string[], now?: number): GraphCuratorResult;
|
|
18
|
+
private projectEpisode;
|
|
17
19
|
private episodeEventIds;
|
|
18
20
|
private facetTargetsFor;
|
|
19
21
|
private upsertFacetNode;
|
|
@@ -22,6 +24,7 @@ export declare class GraphCurator {
|
|
|
22
24
|
private upsertEpisodeRelation;
|
|
23
25
|
private entityHintsFor;
|
|
24
26
|
private deleteFacetEdges;
|
|
27
|
+
private deleteFacetEdgesForEpisodes;
|
|
25
28
|
private upsertEdge;
|
|
26
29
|
}
|
|
27
30
|
//# sourceMappingURL=GraphCurator.d.ts.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { eventTextForMemory } from '../episode/CogmemBlockStripper.js';
|
|
3
3
|
import { EpisodeTitleGenerator } from './EpisodeTitleGenerator.js';
|
|
4
|
+
import { extractEntityCues, normalizeEntityCueId } from '../utils/EntityCueExtractor.js';
|
|
5
|
+
import { inferActionKinds } from '../utils/ActionKindRegistry.js';
|
|
4
6
|
const FACET_EDGE_RELATIONS = new Set([
|
|
5
7
|
'OCCURRED_ON',
|
|
6
8
|
'OCCURRED_IN',
|
|
@@ -37,6 +39,8 @@ export class GraphCurator {
|
|
|
37
39
|
importance,summary,start_event_id,end_event_id,event_count,started_at,updated_at
|
|
38
40
|
FROM memory_episodes
|
|
39
41
|
WHERE project_id=?
|
|
42
|
+
AND COALESCE(event_count,0)>0
|
|
43
|
+
AND status NOT IN ('archived','rejected','merged','invalidated')
|
|
40
44
|
ORDER BY started_at ASC, episode_id ASC
|
|
41
45
|
`).all(projectId);
|
|
42
46
|
const projections = [];
|
|
@@ -123,6 +127,99 @@ export class GraphCurator {
|
|
|
123
127
|
facetEdgeCount += this.projectEpisodeRelations(projectId, projections, now);
|
|
124
128
|
return { episodeCount: rows.length, facetNodeCount, facetEdgeCount, reviewNeeded };
|
|
125
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
|
+
}
|
|
126
223
|
episodeEventIds(episodeId) {
|
|
127
224
|
const rows = this.db.prepare(`SELECT event_id FROM memory_episode_events WHERE episode_id=? ORDER BY position ASC`).all(episodeId);
|
|
128
225
|
return rows.map((row) => row.event_id).filter(Boolean);
|
|
@@ -147,7 +244,10 @@ export class GraphCurator {
|
|
|
147
244
|
targets.push({ type: 'issue', id: issueHint, nodeId: `issue:${projection.row.project_id}:${issueHint}`, label: issueLabel(issueHint), relation: 'PART_OF_ISSUE', confidence: 0.9 });
|
|
148
245
|
}
|
|
149
246
|
for (const entity of this.entityHintsFor(projection)) {
|
|
150
|
-
|
|
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 });
|
|
151
251
|
}
|
|
152
252
|
if (projection.row.session_id) {
|
|
153
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 });
|
|
@@ -157,9 +257,9 @@ export class GraphCurator {
|
|
|
157
257
|
}
|
|
158
258
|
const memoryKind = normalizeKind(projection.issueHints[0] ? issueKind(projection.issueHints[0]) : projection.row.episode_type || 'discussion');
|
|
159
259
|
targets.push({ type: 'memoryKind', id: memoryKind, nodeId: `memoryKind:${projection.row.project_id}:${memoryKind}`, label: memoryKind, relation: 'HAS_MEMORY_KIND', confidence: 0.75 });
|
|
160
|
-
const actionKind
|
|
161
|
-
if (actionKind)
|
|
260
|
+
for (const actionKind of actionKindsFor(projection)) {
|
|
162
261
|
targets.push({ type: 'actionKind', id: actionKind, nodeId: `actionKind:${projection.row.project_id}:${actionKind}`, label: actionKind, relation: 'HAS_ACTION_KIND', confidence: 0.7 });
|
|
262
|
+
}
|
|
163
263
|
for (const eventId of projection.eventIds.slice(0, 30)) {
|
|
164
264
|
targets.push({ type: 'raw_event', id: eventId, nodeId: `raw_event:${eventId}`, label: eventId, relation: 'HAS_EVIDENCE', confidence: 1 });
|
|
165
265
|
}
|
|
@@ -280,6 +380,18 @@ export class GraphCurator {
|
|
|
280
380
|
const relations = Array.from(FACET_EDGE_RELATIONS);
|
|
281
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);
|
|
282
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
|
+
}
|
|
283
395
|
upsertEdge(input) {
|
|
284
396
|
const edgeId = createHash('sha256')
|
|
285
397
|
.update([input.projectId, input.sourceType, input.sourceId, input.relationType, input.targetType, input.targetId].join('\0'))
|
|
@@ -339,21 +451,13 @@ function issueKind(value) {
|
|
|
339
451
|
return 'plan';
|
|
340
452
|
return 'diagnostic';
|
|
341
453
|
}
|
|
342
|
-
function
|
|
454
|
+
function actionKindsFor(projection) {
|
|
343
455
|
const text = projection.events.map(eventTextForMemory).join('\n');
|
|
344
|
-
|
|
345
|
-
return 'implemented';
|
|
346
|
-
if (/review|审查|检查/i.test(text))
|
|
347
|
-
return 'reviewed';
|
|
348
|
-
if (/决定|decided|方案|策略/i.test(text))
|
|
349
|
-
return 'decided';
|
|
350
|
-
if (/debug|排查|卡死|locked|zombie/i.test(text))
|
|
351
|
-
return 'debugged';
|
|
352
|
-
return undefined;
|
|
456
|
+
return inferActionKinds(text);
|
|
353
457
|
}
|
|
354
458
|
function entityHints(events) {
|
|
355
459
|
const text = events.map(eventTextForMemory).join('\n');
|
|
356
|
-
return
|
|
460
|
+
return extractEntityCues(text).map((entity) => entity.label);
|
|
357
461
|
}
|
|
358
462
|
function facetSummary(target) {
|
|
359
463
|
if (target.type === 'topic')
|
|
@@ -22,6 +22,18 @@ export declare class MemoryAtlasIndexer {
|
|
|
22
22
|
curatedEpisodes?: number;
|
|
23
23
|
refreshed: boolean;
|
|
24
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
|
+
};
|
|
25
37
|
ensureAllFresh(): {
|
|
26
38
|
documents: number;
|
|
27
39
|
actions: number;
|
|
@@ -44,6 +44,7 @@ export class MemoryAtlasIndexer {
|
|
|
44
44
|
curatedEpisodes += result.episodeCount;
|
|
45
45
|
facetEdges += result.facetEdgeCount;
|
|
46
46
|
reviewNeeded += result.reviewNeeded;
|
|
47
|
+
this.store.aggregateFacetNodeSupport(id);
|
|
47
48
|
}
|
|
48
49
|
if (projectId) {
|
|
49
50
|
this.store.markProjectionClean(projectId, { actions, curatedEpisodes, facetEdges, reviewNeeded });
|
|
@@ -66,6 +67,41 @@ export class MemoryAtlasIndexer {
|
|
|
66
67
|
}
|
|
67
68
|
return { ...this.rebuild(options), refreshed: true };
|
|
68
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
|
+
}
|
|
69
105
|
ensureAllFresh() {
|
|
70
106
|
let documents = 0;
|
|
71
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
|
}
|
|
@@ -57,8 +57,8 @@ export class MemoryAtlasService {
|
|
|
57
57
|
nodes = uniqueNodes([...actions.map((action) => this.store.getNode(action.id, projectId)).filter((node) => Boolean(node)), ...nodes]).slice(0, limit);
|
|
58
58
|
}
|
|
59
59
|
const nodesWithEvidence = this.attachEvidence(nodes, projectId, options);
|
|
60
|
-
const
|
|
61
|
-
const result = slice(projectId, nodesWithEvidence, edges, query);
|
|
60
|
+
const edgeProjection = this.edgeProjection(nodesWithEvidence, projectId, exactMatchedNodeIds(cards, facetResult.plan));
|
|
61
|
+
const result = slice(projectId, nodesWithEvidence, edgeProjection.edges, query);
|
|
62
62
|
result.facets = {
|
|
63
63
|
...facetsForPlan(facetResult.plan),
|
|
64
64
|
legacy: { time: compiled.range, target: target.labels.join(', ') || compiled.target, memoryKinds: compiled.memoryKinds, keywords: compiled.keywords },
|
|
@@ -68,6 +68,10 @@ export class MemoryAtlasService {
|
|
|
68
68
|
result.cards = this.attachCardEvidence(cards, projectId, options);
|
|
69
69
|
if (facetResult.relaxationTrace.length)
|
|
70
70
|
result.relaxationTrace = facetResult.relaxationTrace;
|
|
71
|
+
if (edgeProjection.truncation) {
|
|
72
|
+
result.edgeTruncation = edgeProjection.truncation;
|
|
73
|
+
result.warnings.push(`edges_truncated:${edgeProjection.truncation.omitted}_omitted`);
|
|
74
|
+
}
|
|
71
75
|
const hasFacet = Boolean(compiled.range || compiled.target || compiled.memoryKinds.length || compiled.tokens.length);
|
|
72
76
|
result.coldMemoryResurrected = hasFacet && nodes.some((node) => node.activation <= 0.1);
|
|
73
77
|
return result;
|
|
@@ -88,7 +92,8 @@ export class MemoryAtlasService {
|
|
|
88
92
|
if (!Number.isInteger(hops) || hops < 1 || hops > 2)
|
|
89
93
|
throw new Error('hops must be between 1 and 2');
|
|
90
94
|
const limit = boundedLimit(options.limit);
|
|
91
|
-
const
|
|
95
|
+
const start = canonicalInputNodeId(this.store, boundedId(nodeId), projectId);
|
|
96
|
+
const seen = new Set([start]);
|
|
92
97
|
let frontier = [...seen];
|
|
93
98
|
const selectedEdges = [];
|
|
94
99
|
for (let depth = 0; depth < hops; depth += 1) {
|
|
@@ -111,8 +116,8 @@ export class MemoryAtlasService {
|
|
|
111
116
|
path(from, to, options) {
|
|
112
117
|
const projectId = requiredProject(options.projectId);
|
|
113
118
|
const maxHops = Math.max(1, Math.min(options.maxHops ?? 6, 6));
|
|
114
|
-
const start = boundedId(from);
|
|
115
|
-
const target = boundedId(to);
|
|
119
|
+
const start = canonicalInputNodeId(this.store, boundedId(from), projectId);
|
|
120
|
+
const target = canonicalInputNodeId(this.store, boundedId(to), projectId);
|
|
116
121
|
const parents = new Map();
|
|
117
122
|
const best = new Map([[start, 0]]);
|
|
118
123
|
const queue = [{ id: start, cost: 0, hops: 0 }];
|
|
@@ -201,13 +206,13 @@ export class MemoryAtlasService {
|
|
|
201
206
|
let plan = this.facetPlanner.plan(boundedQuery(query), { projectId, now: options.now });
|
|
202
207
|
const relaxationTrace = [];
|
|
203
208
|
let cards = plan.facets.length ? this.store.searchCanonicalEpisodeCards(projectId, plan, limit) : [];
|
|
204
|
-
|
|
209
|
+
for (let attempt = 0; !cards.length && plan.facets.length && attempt < 3; attempt += 1) {
|
|
205
210
|
const relaxed = relaxFacetPlan(plan);
|
|
206
|
-
if (relaxed)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
if (!relaxed)
|
|
212
|
+
break;
|
|
213
|
+
plan = relaxed.plan;
|
|
214
|
+
cards = this.store.searchCanonicalEpisodeCards(projectId, plan, limit);
|
|
215
|
+
relaxationTrace.push(...relaxed.trace);
|
|
211
216
|
}
|
|
212
217
|
return { plan, cards, relaxationTrace };
|
|
213
218
|
}
|
|
@@ -247,9 +252,23 @@ export class MemoryAtlasService {
|
|
|
247
252
|
});
|
|
248
253
|
}
|
|
249
254
|
edgesFor(nodes, projectId) {
|
|
255
|
+
return this.edgeProjection(nodes, projectId).edges;
|
|
256
|
+
}
|
|
257
|
+
edgeProjection(nodes, projectId, priorityIds = new Set()) {
|
|
250
258
|
const ids = new Set(nodes.map((node) => node.id));
|
|
251
|
-
|
|
252
|
-
|
|
259
|
+
const limit = 60;
|
|
260
|
+
const candidates = this.safeEdges(this.store.listEdgesWithinNodes(projectId, [...ids], 4000)
|
|
261
|
+
.filter((edge) => ids.has(edge.source) && ids.has(edge.target)), projectId);
|
|
262
|
+
const sorted = uniqueEdges(candidates).sort((left, right) => edgePriority(right, priorityIds) - edgePriority(left, priorityIds)
|
|
263
|
+
|| right.confidence - left.confidence
|
|
264
|
+
|| edgeKey(left).localeCompare(edgeKey(right)));
|
|
265
|
+
const edges = sorted.slice(0, limit);
|
|
266
|
+
return {
|
|
267
|
+
edges,
|
|
268
|
+
truncation: sorted.length > edges.length
|
|
269
|
+
? { limit, returned: edges.length, omitted: sorted.length - edges.length, candidateCount: sorted.length, prioritized: priorityIds.size > 0 }
|
|
270
|
+
: undefined,
|
|
271
|
+
};
|
|
253
272
|
}
|
|
254
273
|
safeEdges(edges, projectId) {
|
|
255
274
|
return edges.map((edge) => ({ ...edge, evidenceEventIds: edge.evidenceEventIds.filter((eventId) => {
|
|
@@ -268,6 +287,17 @@ export class MemoryAtlasService {
|
|
|
268
287
|
.flatMap((chunk) => this.store.findEdgesFromNodesToTarget(projectId, chunk, target)));
|
|
269
288
|
}
|
|
270
289
|
}
|
|
290
|
+
function canonicalInputNodeId(store, id, projectId) {
|
|
291
|
+
if (id.startsWith(`entity:${projectId}:`))
|
|
292
|
+
return id;
|
|
293
|
+
if (!id.startsWith('entity:'))
|
|
294
|
+
return id;
|
|
295
|
+
const entityId = id.slice('entity:'.length);
|
|
296
|
+
if (!entityId.startsWith('facet:'))
|
|
297
|
+
return id;
|
|
298
|
+
const scoped = `entity:${projectId}:${entityId}`;
|
|
299
|
+
return store.getNode(scoped, projectId) ? scoped : id;
|
|
300
|
+
}
|
|
271
301
|
function requiredProject(value) { if (!value?.trim())
|
|
272
302
|
throw new Error('projectId is required for Memory Atlas queries'); return value.trim(); }
|
|
273
303
|
function boundedLimit(value) { if (value !== undefined && (!Number.isFinite(value) || value < 1))
|
|
@@ -282,6 +312,20 @@ function uniqueIds(ids) { return Array.from(new Set(ids)); }
|
|
|
282
312
|
function uniqueEdges(edges) {
|
|
283
313
|
return Array.from(new Map(edges.map((edge) => [`${edge.source}\0${edge.relation}\0${edge.target}`, edge])).values());
|
|
284
314
|
}
|
|
315
|
+
function edgeKey(edge) { return `${edge.source}\0${edge.relation}\0${edge.target}`; }
|
|
316
|
+
function edgePriority(edge, priorityIds) {
|
|
317
|
+
return (priorityIds.has(edge.source) ? 1 : 0) + (priorityIds.has(edge.target) ? 1 : 0);
|
|
318
|
+
}
|
|
319
|
+
function exactMatchedNodeIds(cards, plan) {
|
|
320
|
+
return new Set([
|
|
321
|
+
...cards.flatMap((card) => [
|
|
322
|
+
card.canonicalId,
|
|
323
|
+
...card.matchedFacets.map((facet) => facet.nodeId),
|
|
324
|
+
...card.matchedPaths.flatMap((path) => path.via),
|
|
325
|
+
]),
|
|
326
|
+
...plan.facets.map((facet) => facet.nodeId).filter((id) => Boolean(id)),
|
|
327
|
+
]);
|
|
328
|
+
}
|
|
285
329
|
function edgeTraversalCost(edge) {
|
|
286
330
|
const confidence = Math.max(0.01, Math.min(1, edge.confidence));
|
|
287
331
|
const relationPenalty = /^(EVIDENCED_BY|DERIVED_FROM|TARGETS|OCCURRED_IN|SUPPORTS|ABOUT|MENTIONS)$/u.test(edge.relation)
|
|
@@ -330,23 +374,55 @@ function groupCardsByIssue(cards) {
|
|
|
330
374
|
}
|
|
331
375
|
function relaxFacetPlan(plan) {
|
|
332
376
|
const dayFacet = plan.facets.find((facet) => facet.type === 'time' && facet.granularity === 'day');
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
...plan,
|
|
339
|
-
exactness: 'relaxed',
|
|
340
|
-
facets: plan.facets.map((facet) => facet === dayFacet ? {
|
|
341
|
-
...facet,
|
|
377
|
+
if (dayFacet) {
|
|
378
|
+
const monthValue = dayFacet.value.slice(0, 7);
|
|
379
|
+
return {
|
|
380
|
+
plan: replaceFacet(plan, dayFacet, {
|
|
381
|
+
...dayFacet,
|
|
342
382
|
value: monthValue,
|
|
343
383
|
label: monthValue,
|
|
344
|
-
nodeId:
|
|
384
|
+
nodeId: dayFacet.nodeId?.replace(dayFacet.value, monthValue),
|
|
345
385
|
relation: 'OCCURRED_IN',
|
|
346
386
|
granularity: 'month',
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
387
|
+
}),
|
|
388
|
+
trace: [{ from: dayFacet.value, to: monthValue, reason: 'exact day facet had no canonical episode match; relaxed to parent month' }],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const monthFacet = plan.facets.find((facet) => facet.type === 'time' && facet.granularity === 'month');
|
|
392
|
+
if (monthFacet) {
|
|
393
|
+
const yearValue = monthFacet.value.slice(0, 4);
|
|
394
|
+
return {
|
|
395
|
+
plan: replaceFacet(plan, monthFacet, {
|
|
396
|
+
...monthFacet,
|
|
397
|
+
value: yearValue,
|
|
398
|
+
label: yearValue,
|
|
399
|
+
nodeId: monthFacet.nodeId?.replace(monthFacet.value, yearValue),
|
|
400
|
+
relation: 'OCCURRED_IN',
|
|
401
|
+
granularity: 'year',
|
|
402
|
+
}),
|
|
403
|
+
trace: [{ from: monthFacet.value, to: yearValue, reason: 'exact month facet had no canonical episode match; relaxed to parent year' }],
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const issueFacet = plan.facets.find((facet) => facet.type === 'issue');
|
|
407
|
+
const topicFacet = plan.facets.find((facet) => facet.type === 'topic');
|
|
408
|
+
if (issueFacet && topicFacet) {
|
|
409
|
+
return {
|
|
410
|
+
plan: {
|
|
411
|
+
...plan,
|
|
412
|
+
exactness: 'relaxed',
|
|
413
|
+
requiresIntersection: plan.facets.length - 1 > 1,
|
|
414
|
+
facets: plan.facets.filter((facet) => facet !== issueFacet),
|
|
415
|
+
},
|
|
416
|
+
trace: [{ from: issueFacet.value, to: topicFacet.value, reason: 'issue facet had no canonical episode match; relaxed to parent topic' }],
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
function replaceFacet(plan, from, to) {
|
|
422
|
+
return {
|
|
423
|
+
...plan,
|
|
424
|
+
exactness: 'relaxed',
|
|
425
|
+
facets: plan.facets.map((facet) => facet === from ? to : facet),
|
|
350
426
|
};
|
|
351
427
|
}
|
|
352
428
|
function atlasSourceLocator(event, projectId) {
|
|
@@ -101,6 +101,13 @@ export interface MemoryAtlasSlice {
|
|
|
101
101
|
edges: MemoryAtlasEdge[];
|
|
102
102
|
nextActions: MemoryAtlasNextAction[];
|
|
103
103
|
warnings: string[];
|
|
104
|
+
edgeTruncation?: {
|
|
105
|
+
limit: number;
|
|
106
|
+
returned: number;
|
|
107
|
+
omitted: number;
|
|
108
|
+
candidateCount: number;
|
|
109
|
+
prioritized: boolean;
|
|
110
|
+
};
|
|
104
111
|
facets?: {
|
|
105
112
|
planner?: {
|
|
106
113
|
intent: string;
|