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.
Files changed (45) hide show
  1. package/CHANGELOG.md +18 -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.d.ts +1 -0
  23. package/dist/atlas/MemoryAtlasService.js +102 -26
  24. package/dist/atlas/MemoryAtlasTypes.d.ts +7 -0
  25. package/dist/bin/memory.js +14 -4
  26. package/dist/factory.d.ts +18 -0
  27. package/dist/factory.js +41 -2
  28. package/dist/host/openclaw/AutoMemoryPluginInstaller.js +71 -44
  29. package/dist/mcp/server.js +1 -1
  30. package/dist/recall/RecallExplanation.d.ts +1 -1
  31. package/dist/store/MemoryAtlasStore.d.ts +4 -1
  32. package/dist/store/MemoryAtlasStore.js +203 -25
  33. package/dist/utils/ActionKindRegistry.d.ts +10 -0
  34. package/dist/utils/ActionKindRegistry.js +18 -0
  35. package/dist/utils/EntityCueExtractor.d.ts +13 -0
  36. package/dist/utils/EntityCueExtractor.js +81 -0
  37. package/examples/hermes-backend/AGENTS.md +1 -1
  38. package/examples/hermes-backend/README.md +1 -1
  39. package/examples/hermes-backend/SKILL.md +10 -3
  40. package/examples/hermes-backend/references/operations.md +9 -8
  41. package/examples/openclaw-backend/AGENTS.md +1 -1
  42. package/examples/openclaw-backend/README.md +2 -2
  43. package/examples/openclaw-backend/SKILL.md +24 -7
  44. package/examples/openclaw-backend/references/operations.md +26 -10
  45. 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
  }
@@ -22,6 +22,7 @@ export declare class MemoryAtlasService {
22
22
  private attachCardEvidence;
23
23
  private evidence;
24
24
  private edgesFor;
25
+ private edgeProjection;
25
26
  private safeEdges;
26
27
  private adjacentEdges;
27
28
  private directEdgesToTarget;
@@ -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 edges = this.edgesFor(nodesWithEvidence, projectId);
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 seen = new Set([boundedId(nodeId)]);
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
- if (!cards.length && plan.facets.length) {
209
+ for (let attempt = 0; !cards.length && plan.facets.length && attempt < 3; attempt += 1) {
205
210
  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
- }
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
- return this.safeEdges(this.store.listEdgesForNodes(projectId, [...ids], 60)
252
- .filter((edge) => ids.has(edge.source) && ids.has(edge.target)).slice(0, 60), projectId);
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 (!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,
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: facet.nodeId?.replace(dayFacet.value, monthValue),
384
+ nodeId: dayFacet.nodeId?.replace(dayFacet.value, monthValue),
345
385
  relation: 'OCCURRED_IN',
346
386
  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' }],
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;