cogmem 3.7.0 → 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 +12 -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.js +66 -22
- 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 +3 -1
- package/dist/store/MemoryAtlasStore.js +149 -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
|
}
|
|
@@ -88,7 +88,8 @@ export class MemoryAtlasService {
|
|
|
88
88
|
if (!Number.isInteger(hops) || hops < 1 || hops > 2)
|
|
89
89
|
throw new Error('hops must be between 1 and 2');
|
|
90
90
|
const limit = boundedLimit(options.limit);
|
|
91
|
-
const
|
|
91
|
+
const start = canonicalInputNodeId(this.store, boundedId(nodeId), projectId);
|
|
92
|
+
const seen = new Set([start]);
|
|
92
93
|
let frontier = [...seen];
|
|
93
94
|
const selectedEdges = [];
|
|
94
95
|
for (let depth = 0; depth < hops; depth += 1) {
|
|
@@ -111,8 +112,8 @@ export class MemoryAtlasService {
|
|
|
111
112
|
path(from, to, options) {
|
|
112
113
|
const projectId = requiredProject(options.projectId);
|
|
113
114
|
const maxHops = Math.max(1, Math.min(options.maxHops ?? 6, 6));
|
|
114
|
-
const start = boundedId(from);
|
|
115
|
-
const target = boundedId(to);
|
|
115
|
+
const start = canonicalInputNodeId(this.store, boundedId(from), projectId);
|
|
116
|
+
const target = canonicalInputNodeId(this.store, boundedId(to), projectId);
|
|
116
117
|
const parents = new Map();
|
|
117
118
|
const best = new Map([[start, 0]]);
|
|
118
119
|
const queue = [{ id: start, cost: 0, hops: 0 }];
|
|
@@ -201,13 +202,13 @@ export class MemoryAtlasService {
|
|
|
201
202
|
let plan = this.facetPlanner.plan(boundedQuery(query), { projectId, now: options.now });
|
|
202
203
|
const relaxationTrace = [];
|
|
203
204
|
let cards = plan.facets.length ? this.store.searchCanonicalEpisodeCards(projectId, plan, limit) : [];
|
|
204
|
-
|
|
205
|
+
for (let attempt = 0; !cards.length && plan.facets.length && attempt < 3; attempt += 1) {
|
|
205
206
|
const relaxed = relaxFacetPlan(plan);
|
|
206
|
-
if (relaxed)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
if (!relaxed)
|
|
208
|
+
break;
|
|
209
|
+
plan = relaxed.plan;
|
|
210
|
+
cards = this.store.searchCanonicalEpisodeCards(projectId, plan, limit);
|
|
211
|
+
relaxationTrace.push(...relaxed.trace);
|
|
211
212
|
}
|
|
212
213
|
return { plan, cards, relaxationTrace };
|
|
213
214
|
}
|
|
@@ -268,6 +269,17 @@ export class MemoryAtlasService {
|
|
|
268
269
|
.flatMap((chunk) => this.store.findEdgesFromNodesToTarget(projectId, chunk, target)));
|
|
269
270
|
}
|
|
270
271
|
}
|
|
272
|
+
function canonicalInputNodeId(store, id, projectId) {
|
|
273
|
+
if (id.startsWith(`entity:${projectId}:`))
|
|
274
|
+
return id;
|
|
275
|
+
if (!id.startsWith('entity:'))
|
|
276
|
+
return id;
|
|
277
|
+
const entityId = id.slice('entity:'.length);
|
|
278
|
+
if (!entityId.startsWith('facet:'))
|
|
279
|
+
return id;
|
|
280
|
+
const scoped = `entity:${projectId}:${entityId}`;
|
|
281
|
+
return store.getNode(scoped, projectId) ? scoped : id;
|
|
282
|
+
}
|
|
271
283
|
function requiredProject(value) { if (!value?.trim())
|
|
272
284
|
throw new Error('projectId is required for Memory Atlas queries'); return value.trim(); }
|
|
273
285
|
function boundedLimit(value) { if (value !== undefined && (!Number.isFinite(value) || value < 1))
|
|
@@ -330,23 +342,55 @@ function groupCardsByIssue(cards) {
|
|
|
330
342
|
}
|
|
331
343
|
function relaxFacetPlan(plan) {
|
|
332
344
|
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,
|
|
345
|
+
if (dayFacet) {
|
|
346
|
+
const monthValue = dayFacet.value.slice(0, 7);
|
|
347
|
+
return {
|
|
348
|
+
plan: replaceFacet(plan, dayFacet, {
|
|
349
|
+
...dayFacet,
|
|
342
350
|
value: monthValue,
|
|
343
351
|
label: monthValue,
|
|
344
|
-
nodeId:
|
|
352
|
+
nodeId: dayFacet.nodeId?.replace(dayFacet.value, monthValue),
|
|
345
353
|
relation: 'OCCURRED_IN',
|
|
346
354
|
granularity: 'month',
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
355
|
+
}),
|
|
356
|
+
trace: [{ from: dayFacet.value, to: monthValue, reason: 'exact day facet had no canonical episode match; relaxed to parent month' }],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const monthFacet = plan.facets.find((facet) => facet.type === 'time' && facet.granularity === 'month');
|
|
360
|
+
if (monthFacet) {
|
|
361
|
+
const yearValue = monthFacet.value.slice(0, 4);
|
|
362
|
+
return {
|
|
363
|
+
plan: replaceFacet(plan, monthFacet, {
|
|
364
|
+
...monthFacet,
|
|
365
|
+
value: yearValue,
|
|
366
|
+
label: yearValue,
|
|
367
|
+
nodeId: monthFacet.nodeId?.replace(monthFacet.value, yearValue),
|
|
368
|
+
relation: 'OCCURRED_IN',
|
|
369
|
+
granularity: 'year',
|
|
370
|
+
}),
|
|
371
|
+
trace: [{ from: monthFacet.value, to: yearValue, reason: 'exact month facet had no canonical episode match; relaxed to parent year' }],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const issueFacet = plan.facets.find((facet) => facet.type === 'issue');
|
|
375
|
+
const topicFacet = plan.facets.find((facet) => facet.type === 'topic');
|
|
376
|
+
if (issueFacet && topicFacet) {
|
|
377
|
+
return {
|
|
378
|
+
plan: {
|
|
379
|
+
...plan,
|
|
380
|
+
exactness: 'relaxed',
|
|
381
|
+
requiresIntersection: plan.facets.length - 1 > 1,
|
|
382
|
+
facets: plan.facets.filter((facet) => facet !== issueFacet),
|
|
383
|
+
},
|
|
384
|
+
trace: [{ from: issueFacet.value, to: topicFacet.value, reason: 'issue facet had no canonical episode match; relaxed to parent topic' }],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
function replaceFacet(plan, from, to) {
|
|
390
|
+
return {
|
|
391
|
+
...plan,
|
|
392
|
+
exactness: 'relaxed',
|
|
393
|
+
facets: plan.facets.map((facet) => facet === from ? to : facet),
|
|
350
394
|
};
|
|
351
395
|
}
|
|
352
396
|
function atlasSourceLocator(event, projectId) {
|
package/dist/bin/memory.js
CHANGED
|
@@ -28,6 +28,7 @@ function readArgs(argv) {
|
|
|
28
28
|
command,
|
|
29
29
|
query: stringArg(values, 'query') || stringArg(values, 'q'),
|
|
30
30
|
eventId: stringArg(values, 'event') || stringArg(values, 'event-id'),
|
|
31
|
+
episodeId: stringArg(values, 'episode') || stringArg(values, 'episode-id'),
|
|
31
32
|
nodeId: stringArg(values, 'id') || stringArg(values, 'node-id'),
|
|
32
33
|
fromId: stringArg(values, 'from'),
|
|
33
34
|
toId: stringArg(values, 'to'),
|
|
@@ -96,6 +97,7 @@ function usage() {
|
|
|
96
97
|
' graph-neighbors expand --id by --hops 1..2',
|
|
97
98
|
' graph-path find a bounded path from --from to --to',
|
|
98
99
|
' graph-timeline reconstruct entity/time/action history for --query',
|
|
100
|
+
' graph-reindex reproject one Atlas episode/card by --event or --episode without rebuilding the whole graph',
|
|
99
101
|
'',
|
|
100
102
|
'Common options:',
|
|
101
103
|
' --project <id> scope to one project',
|
|
@@ -122,7 +124,7 @@ function usage() {
|
|
|
122
124
|
' --interval-ms <n> watch sleep interval, default 300000',
|
|
123
125
|
' --max-runs <n> stop watch after n iterations; omit for long-running worker',
|
|
124
126
|
' --agent <id> agent id for governed recall, default openclaw',
|
|
125
|
-
' --intent <intent> memory_recall, previous_session_summary, forensic_quote, or
|
|
127
|
+
' --intent <intent> memory_recall, previous_session_summary, forensic_quote, historical_discussion, or action_history',
|
|
126
128
|
' --db <memory.db> open an explicit database path',
|
|
127
129
|
' --config <toml> open a cogmem TOML config',
|
|
128
130
|
' --include-evidence include bounded raw excerpts; event ids are always returned',
|
|
@@ -158,7 +160,8 @@ function isMemoryCommand(value) {
|
|
|
158
160
|
|| value === 'graph-node'
|
|
159
161
|
|| value === 'graph-neighbors'
|
|
160
162
|
|| value === 'graph-path'
|
|
161
|
-
|| value === 'graph-timeline'
|
|
163
|
+
|| value === 'graph-timeline'
|
|
164
|
+
|| value === 'graph-reindex';
|
|
162
165
|
}
|
|
163
166
|
function reviewActionArg(values, key) {
|
|
164
167
|
const raw = stringArg(values, key);
|
|
@@ -172,9 +175,9 @@ function recallIntentArg(values, key) {
|
|
|
172
175
|
const raw = stringArg(values, key);
|
|
173
176
|
if (!raw)
|
|
174
177
|
return undefined;
|
|
175
|
-
if (raw === 'memory_recall' || raw === 'previous_session_summary' || raw === 'forensic_quote' || raw === 'historical_discussion')
|
|
178
|
+
if (raw === 'memory_recall' || raw === 'previous_session_summary' || raw === 'forensic_quote' || raw === 'historical_discussion' || raw === 'action_history')
|
|
176
179
|
return raw;
|
|
177
|
-
throw new Error(`--${key} must be one of memory_recall, previous_session_summary, forensic_quote, historical_discussion`);
|
|
180
|
+
throw new Error(`--${key} must be one of memory_recall, previous_session_summary, forensic_quote, historical_discussion, action_history`);
|
|
178
181
|
}
|
|
179
182
|
function orderArg(values, key) {
|
|
180
183
|
const raw = stringArg(values, key);
|
|
@@ -603,6 +606,13 @@ function runGraphCommand(kernel, args) {
|
|
|
603
606
|
evidenceLimit: args.evidenceLimit, now: args.now,
|
|
604
607
|
refresh: args.refresh ? true : (args.staleOk ? false : undefined),
|
|
605
608
|
staleOk: args.staleOk || !args.refresh };
|
|
609
|
+
if (args.command === 'graph-reindex') {
|
|
610
|
+
if (!args.eventId && !args.episodeId)
|
|
611
|
+
throw new Error(`graph-reindex requires --event or --episode.\n${usage()}`);
|
|
612
|
+
if (args.eventId && args.episodeId)
|
|
613
|
+
throw new Error(`graph-reindex accepts exactly one of --event or --episode.\n${usage()}`);
|
|
614
|
+
return kernel.reindexMemoryAtlas({ projectId, eventId: args.eventId, episodeId: args.episodeId });
|
|
615
|
+
}
|
|
606
616
|
if (args.command === 'graph')
|
|
607
617
|
return kernel.graphOverview(options);
|
|
608
618
|
if (args.command === 'graph-search') {
|
package/dist/factory.d.ts
CHANGED
|
@@ -326,9 +326,15 @@ export type EpisodeRepairInput = {
|
|
|
326
326
|
};
|
|
327
327
|
export interface EpisodeRepairResult {
|
|
328
328
|
repairId: string;
|
|
329
|
+
applied: boolean;
|
|
329
330
|
operation: EpisodeRepairInput['operation'];
|
|
330
331
|
affectedEpisodeIds: string[];
|
|
332
|
+
changedFields: string[];
|
|
331
333
|
staleCandidateIds: string[];
|
|
334
|
+
requeuedDream: boolean;
|
|
335
|
+
graphRefreshNeeded: boolean;
|
|
336
|
+
nextCommands: string[];
|
|
337
|
+
note: string;
|
|
332
338
|
}
|
|
333
339
|
export interface ToolCallMemoryEventInput {
|
|
334
340
|
projectId?: string;
|
|
@@ -635,6 +641,18 @@ export declare class MemoryKernel {
|
|
|
635
641
|
documents: number;
|
|
636
642
|
actions: number;
|
|
637
643
|
};
|
|
644
|
+
reindexMemoryAtlas(options: {
|
|
645
|
+
projectId: string;
|
|
646
|
+
eventId?: string;
|
|
647
|
+
episodeId?: string;
|
|
648
|
+
}): {
|
|
649
|
+
projectId: string;
|
|
650
|
+
episodeIds: string[];
|
|
651
|
+
refreshed: boolean;
|
|
652
|
+
curatedEpisodes: number;
|
|
653
|
+
facetEdges: number;
|
|
654
|
+
reviewNeeded: number;
|
|
655
|
+
};
|
|
638
656
|
ensureMemoryAtlas(options: {
|
|
639
657
|
projectId: string;
|
|
640
658
|
}): {
|