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.
Files changed (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/MEMORY_ATLAS.md +16 -6
  3. package/README.md +16 -11
  4. package/RELEASE_CHECKLIST.md +4 -4
  5. package/dist/agent/AgentMemoryBackend.d.ts +11 -2
  6. package/dist/agent/AgentMemoryBackend.js +185 -36
  7. package/dist/agent/AgentRecallQueryCompiler.d.ts +1 -1
  8. package/dist/agent/AgentRecallQueryCompiler.js +13 -1
  9. package/dist/agent/MemoryUsageReceipt.js +8 -10
  10. package/dist/agent/SessionWorkingState.js +3 -2
  11. package/dist/agent/UntrustedMemorySerializer.d.ts +2 -0
  12. package/dist/agent/UntrustedMemorySerializer.js +14 -0
  13. package/dist/atlas/ActionFrameExtractor.js +2 -0
  14. package/dist/atlas/EpisodeTitleGenerator.js +42 -3
  15. package/dist/atlas/FacetQueryPlanner.d.ts +2 -0
  16. package/dist/atlas/FacetQueryPlanner.js +57 -16
  17. package/dist/atlas/GraphCurator.d.ts +3 -0
  18. package/dist/atlas/GraphCurator.js +118 -14
  19. package/dist/atlas/MemoryAtlasIndexer.d.ts +12 -0
  20. package/dist/atlas/MemoryAtlasIndexer.js +36 -0
  21. package/dist/atlas/MemoryAtlasQueryCompiler.js +10 -7
  22. package/dist/atlas/MemoryAtlasService.js +66 -22
  23. package/dist/bin/memory.js +14 -4
  24. package/dist/factory.d.ts +18 -0
  25. package/dist/factory.js +41 -2
  26. package/dist/host/openclaw/AutoMemoryPluginInstaller.js +71 -44
  27. package/dist/mcp/server.js +1 -1
  28. package/dist/recall/RecallExplanation.d.ts +1 -1
  29. package/dist/store/MemoryAtlasStore.d.ts +3 -1
  30. package/dist/store/MemoryAtlasStore.js +149 -25
  31. package/dist/utils/ActionKindRegistry.d.ts +10 -0
  32. package/dist/utils/ActionKindRegistry.js +18 -0
  33. package/dist/utils/EntityCueExtractor.d.ts +13 -0
  34. package/dist/utils/EntityCueExtractor.js +81 -0
  35. package/examples/hermes-backend/AGENTS.md +1 -1
  36. package/examples/hermes-backend/README.md +1 -1
  37. package/examples/hermes-backend/SKILL.md +10 -3
  38. package/examples/hermes-backend/references/operations.md +9 -8
  39. package/examples/openclaw-backend/AGENTS.md +1 -1
  40. package/examples/openclaw-backend/README.md +2 -2
  41. package/examples/openclaw-backend/SKILL.md +24 -7
  42. package/examples/openclaw-backend/references/operations.md +26 -10
  43. 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|原文下钻|原话|摘要.*原文)/i.test(text),
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 timeFacet = parseTimeFacet(normalized, options.now ?? Date.now());
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 ['Cogmem', 'OpenClaw', 'Hermes']) {
64
- if (new RegExp(entity, 'i').test(normalized)) {
65
- facets.push({ type: 'entity', value: `facet:${entity.toLowerCase()}`, label: entity, nodeId: `entity:facet:${entity.toLowerCase()}`, relation: 'INVOLVES_ENTITY' });
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 [value, test] of actionKindRules) {
104
- if (test.test(query))
105
- facets.push(withNodeId({ type: 'actionKind', value, label: value, relation: 'HAS_ACTION_KIND' }, projectId));
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, now) {
110
- const currentYear = new Date(now).getUTCFullYear();
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
- targets.push({ type: 'entity', id: `facet:${entity.toLowerCase()}`, nodeId: `entity:facet:${entity.toLowerCase()}`, label: entity, relation: 'INVOLVES_ENTITY', confidence: 0.78 });
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 = actionKindFor(projection);
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 actionKindFor(projection) {
454
+ function actionKindsFor(projection) {
343
455
  const text = projection.events.map(eventTextForMemory).join('\n');
344
- if (/修复|fixed|implemented|实现|提交|升级|upgrade/i.test(text))
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 ['Cogmem', 'OpenClaw', 'Hermes'].filter((entity) => new RegExp(entity, 'i').test(text));
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 = /配置|连接|安装|修复|更新|升级|比较|操作|设置|调试|configure|connect|install|repair|fix|update|upgrade|compare|setup|debug/iu;
2
+ const ACTION_MARKERS = /启动|重启|停止|执行|配置|连接|安装|修复|更新|升级|比较|操作|设置|调试|start|started|launch|launched|boot|restart|restarted|stop|stopped|run|ran|configure|connect|install|repair|fix|update|upgrade|compare|setup|debug/iu;
3
3
  const MEMORY_KIND_MARKERS = [
4
4
  [/决策|决定|decision/iu, 'decision'], [/修正|纠正|correction/iu, 'correction'],
5
5
  [/目标|goal/iu, 'goal'], [/偏好|preference/iu, 'preference'], [/计划|plan/iu, 'plan'],
@@ -45,11 +45,14 @@ export function actionMarkers(value) {
45
45
  }
46
46
  function frameTypeForAction(lower) {
47
47
  const frameType = /修复|fix|repair|调试|debug/u.test(lower) ? 'repair'
48
- : /安装|install/u.test(lower) ? 'install'
49
- : /连接|connect/u.test(lower) ? 'connect'
50
- : /更新|升级|update|upgrade/u.test(lower) ? 'update'
51
- : /比较|compare/u.test(lower) ? 'compare'
52
- : /配置|设置|configure|setup/u.test(lower) ? 'configuration'
53
- : 'operation';
48
+ : /启动|start|launch|boot/u.test(lower) ? 'start'
49
+ : /重启|restart/u.test(lower) ? 'restart'
50
+ : /停止|stop/u.test(lower) ? 'stop'
51
+ : /安装|install/u.test(lower) ? 'install'
52
+ : /连接|connect/u.test(lower) ? 'connect'
53
+ : /更新|升级|update|upgrade/u.test(lower) ? 'update'
54
+ : /比较|compare/u.test(lower) ? 'compare'
55
+ : /配置|设置|configure|setup/u.test(lower) ? 'configuration'
56
+ : 'operation';
54
57
  return frameType;
55
58
  }
@@ -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 seen = new Set([boundedId(nodeId)]);
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
- if (!cards.length && plan.facets.length) {
205
+ for (let attempt = 0; !cards.length && plan.facets.length && attempt < 3; attempt += 1) {
205
206
  const relaxed = relaxFacetPlan(plan);
206
- if (relaxed) {
207
- plan = relaxed.plan;
208
- cards = this.store.searchCanonicalEpisodeCards(projectId, relaxed.plan, limit);
209
- relaxationTrace.push(...relaxed.trace);
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 (!dayFacet)
334
- return null;
335
- const monthValue = dayFacet.value.slice(0, 7);
336
- return {
337
- plan: {
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: facet.nodeId?.replace(dayFacet.value, monthValue),
352
+ nodeId: dayFacet.nodeId?.replace(dayFacet.value, monthValue),
345
353
  relation: 'OCCURRED_IN',
346
354
  granularity: 'month',
347
- } : facet),
348
- },
349
- trace: [{ from: dayFacet.value, to: monthValue, reason: 'exact day facet had no canonical episode match; relaxed to parent month' }],
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) {
@@ -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 historical_discussion',
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
  }): {