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,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  export const MEMORY_ATLAS_PROJECTION_NAME = 'memory_atlas.v2';
3
- export const MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION = '3.7.0';
3
+ export const MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION = '3.7.2';
4
4
  export class MemoryAtlasStore {
5
5
  db;
6
6
  constructor(db) {
@@ -23,12 +23,13 @@ export class MemoryAtlasStore {
23
23
  this.refreshFtsNode(input.id);
24
24
  }
25
25
  getNode(nodeId, projectId) {
26
+ const scopedNodeId = scopedEntityNodeId(this.db, nodeId, projectId);
26
27
  const row = this.db.prepare(`
27
28
  SELECT d.*, COALESCE(a.activation, 0) AS activation
28
29
  FROM memory_atlas_documents d LEFT JOIN memory_atlas_activation a
29
30
  ON a.project_id=d.project_id AND a.node_id=d.node_id
30
31
  WHERE d.node_id=? AND d.project_id=?
31
- `).get(nodeId, projectId);
32
+ `).get(scopedNodeId, projectId);
32
33
  return row ? mapNode(row, 0) : null;
33
34
  }
34
35
  listNodes(projectId, limit) {
@@ -89,23 +90,25 @@ export class MemoryAtlasStore {
89
90
  if (!plannedFacets.length)
90
91
  return [];
91
92
  const facetMatches = plannedFacets.map((facet) => ({ facet, rows: this.episodeEdgesForFacet(projectId, facet) }));
92
- if (plan.operator === 'intersection' && facetMatches.some((match) => match.rows.length === 0))
93
+ const facetGroups = groupFacetMatches(facetMatches);
94
+ if (plan.operator === 'intersection' && facetGroups.some((group) => group.every((match) => match.rows.length === 0)))
93
95
  return [];
94
96
  const candidateIds = plan.operator === 'intersection'
95
- ? intersectSets(facetMatches.map((match) => new Set(match.rows.map((row) => row.source_id))))
97
+ ? intersectSets(facetGroups.map((group) => new Set(group.flatMap((match) => match.rows.map((row) => row.source_id)))))
96
98
  : new Set(facetMatches.flatMap((match) => match.rows.map((row) => row.source_id)));
97
99
  if (!candidateIds.size)
98
100
  return [];
99
101
  const rows = this.episodeRows(projectId, Array.from(candidateIds));
100
- const cards = rows.map((row) => {
102
+ const rankedCards = rows.map((row) => {
101
103
  const candidateEdges = facetMatches.flatMap((match) => match.rows.filter((edge) => edge.source_id === row.source_id));
102
104
  return this.cardFromEpisodeRow(row, candidateEdges, projectId);
103
- });
105
+ }).sort((left, right) => cardRank(right) - cardRank(left) || left.canonicalId.localeCompare(right.canonicalId));
106
+ const cards = dedupeCardsByEvidence(rankedCards).slice(0, Math.max(1, Math.min(limit, 100)));
104
107
  const selectedIds = new Set(cards.map((card) => card.canonicalId));
105
108
  for (const card of cards) {
106
- card.relatedButNotSelected = mergeRelatedCards(this.relatedEpisodeCards(projectId, card.canonicalId, selectedIds, 4), this.topicRelatedCardsForPlan(projectId, plan, selectedIds, 4)).slice(0, 4);
109
+ card.relatedButNotSelected = mergeRelatedCards(card.relatedButNotSelected ?? [], this.relatedEpisodeCards(projectId, card.canonicalId, selectedIds, 4), this.topicRelatedCardsForPlan(projectId, plan, selectedIds, 4)).slice(0, 4);
107
110
  }
108
- return cards.sort((left, right) => scoreCard(right) - scoreCard(left)).slice(0, Math.max(1, Math.min(limit, 100)));
111
+ return cards;
109
112
  }
110
113
  relatedEpisodeCards(projectId, canonicalId, selectedIds, limit) {
111
114
  const parsed = parseNodeId(canonicalId, projectId);
@@ -298,6 +301,54 @@ export class MemoryAtlasStore {
298
301
  edges.push(...this.topicRelationEdges(projectId, topicPaths, Math.max(1, Math.min(limit, 2000))));
299
302
  return Array.from(new Map(edges.map((edge) => [`${edge.source}\0${edge.relation}\0${edge.target}`, edge])).values()).slice(0, Math.max(1, Math.min(limit, 4000)));
300
303
  }
304
+ listEdgesWithinNodes(projectId, nodeIds, limit = 4000) {
305
+ const selectedIds = new Set(Array.from(new Set(nodeIds)));
306
+ const parsed = Array.from(selectedIds).map((id) => parseNodeId(id, projectId)).filter((item) => Boolean(item));
307
+ if (!parsed.length)
308
+ return [];
309
+ const edges = [];
310
+ const cap = Math.max(1, Math.min(limit, 4000));
311
+ for (const sourceChunk of chunked(parsed, 120)) {
312
+ for (const targetChunk of chunked(parsed, 120)) {
313
+ const sourceClause = sourceChunk.map(() => '(source_type=? AND source_id=?)').join(' OR ');
314
+ const targetClause = targetChunk.map(() => '(target_type=? AND target_id=?)').join(' OR ');
315
+ const rows = this.db.prepare(`
316
+ SELECT * FROM memory_edges
317
+ WHERE project_id=? AND status IN ('active','weak')
318
+ AND (${sourceClause}) AND (${targetClause})
319
+ ORDER BY confidence DESC LIMIT ?
320
+ `).all(projectId, ...sourceChunk.flatMap((item) => [item.type, item.id]), ...targetChunk.flatMap((item) => [item.type, item.id]), cap);
321
+ for (const row of rows)
322
+ edges.push({
323
+ source: nodeId(String(row.source_type), String(row.source_id), projectId), relation: String(row.relation_type),
324
+ target: nodeId(String(row.target_type), String(row.target_id), projectId), confidence: Number(row.confidence),
325
+ evidenceEventIds: parseStringArray(String(row.evidence_event_ids_json || '[]')),
326
+ });
327
+ }
328
+ }
329
+ const actionIds = parsed.filter((item) => item.type === 'action').map((item) => item.id);
330
+ if (actionIds.length) {
331
+ for (const chunk of chunked(actionIds, 400)) {
332
+ const actions = this.db.prepare(`SELECT action_id,target_entity_id,occurred_at,confidence FROM memory_action_frames
333
+ WHERE project_id=? AND action_id IN (${chunk.map(() => '?').join(',')})`).all(projectId, ...chunk);
334
+ for (const action of actions) {
335
+ const evidenceEventIds = this.actionEvidenceIds(action.action_id, projectId);
336
+ if (action.target_entity_id && selectedIds.has(`entity:${action.target_entity_id}`)) {
337
+ edges.push({ source: `action:${action.action_id}`, relation: 'TARGETS', target: `entity:${action.target_entity_id}`, confidence: action.confidence, evidenceEventIds });
338
+ }
339
+ const timeId = timeNodeId(projectId, action.occurred_at);
340
+ if (selectedIds.has(timeId))
341
+ edges.push({ source: `action:${action.action_id}`, relation: 'OCCURRED_IN', target: timeId, confidence: 1, evidenceEventIds });
342
+ }
343
+ }
344
+ }
345
+ const topicPaths = parsed.filter((item) => item.type === 'topic').map((item) => item.id);
346
+ if (topicPaths.length) {
347
+ edges.push(...this.topicRelationEdges(projectId, topicPaths, cap)
348
+ .filter((edge) => selectedIds.has(edge.source) && selectedIds.has(edge.target)));
349
+ }
350
+ return Array.from(new Map(edges.map((edge) => [`${edge.source}\0${edge.relation}\0${edge.target}`, edge])).values());
351
+ }
301
352
  findEdgesBetween(projectId, leftNodeId, rightNodeId) {
302
353
  const left = parseNodeId(leftNodeId, projectId);
303
354
  const right = parseNodeId(rightNodeId, projectId);
@@ -483,6 +534,47 @@ export class MemoryAtlasStore {
483
534
  last_error=NULL, metadata_json=excluded.metadata_json
484
535
  `).run(projectId, String(now), now, JSON.stringify({ compatibilityAliasFor: MEMORY_ATLAS_PROJECTION_NAME }));
485
536
  }
537
+ markProjectionDirty(projectId, metadata = {}, now = Date.now()) {
538
+ this.db.prepare(`
539
+ INSERT INTO memory_atlas_projection_state(project_id,projection_name,cursor_value,status,last_rebuild_at,last_error,metadata_json)
540
+ VALUES(?, ?, NULL, 'dirty', ?, NULL, ?)
541
+ ON CONFLICT(project_id,projection_name) DO UPDATE SET
542
+ status='dirty', cursor_value=NULL, last_error=NULL, metadata_json=excluded.metadata_json
543
+ `).run(projectId, MEMORY_ATLAS_PROJECTION_NAME, now, JSON.stringify({
544
+ ...metadata,
545
+ projectionName: MEMORY_ATLAS_PROJECTION_NAME,
546
+ projectionSchemaVersion: MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION,
547
+ }));
548
+ this.db.prepare(`
549
+ INSERT INTO memory_atlas_projection_state(project_id,projection_name,cursor_value,status,last_rebuild_at,last_error,metadata_json)
550
+ VALUES(?, 'memory_atlas.v1', NULL, 'dirty', ?, NULL, ?)
551
+ ON CONFLICT(project_id,projection_name) DO UPDATE SET status='dirty', cursor_value=NULL, last_error=NULL, metadata_json=excluded.metadata_json
552
+ `).run(projectId, now, JSON.stringify({ compatibilityAliasFor: MEMORY_ATLAS_PROJECTION_NAME, dirtyBecause: 'atlas_v2_targeted_reindex' }));
553
+ }
554
+ aggregateFacetNodeSupport(projectId, now = Date.now()) {
555
+ const rows = this.db.prepare(`
556
+ SELECT target_type,target_id,evidence_event_ids_json
557
+ FROM memory_edges
558
+ WHERE project_id=? AND source_type='episode'
559
+ AND target_type IN ('topic','time','issue','entity','session','thread','memoryKind','actionKind')
560
+ AND status IN ('active','weak')
561
+ ORDER BY target_type,target_id
562
+ `).all(projectId);
563
+ const buckets = new Map();
564
+ for (const row of rows) {
565
+ const id = nodeId(row.target_type, row.target_id, projectId);
566
+ const bucket = buckets.get(id) ?? { count: 0, evidence: new Set() };
567
+ bucket.count += 1;
568
+ for (const eventId of parseStringArray(row.evidence_event_ids_json).slice(0, 5))
569
+ bucket.evidence.add(eventId);
570
+ buckets.set(id, bucket);
571
+ }
572
+ const update = this.db.prepare(`UPDATE memory_atlas_documents SET support_count=?, evidence_event_ids_json=?, updated_at=? WHERE project_id=? AND node_id=?`);
573
+ for (const [nodeIdValue, bucket] of buckets) {
574
+ update.run(bucket.count, JSON.stringify(Array.from(bucket.evidence).slice(0, 100)), now, projectId, nodeIdValue);
575
+ this.refreshFtsNode(nodeIdValue);
576
+ }
577
+ }
486
578
  markProjectionFailed(projectId, error, now = Date.now()) {
487
579
  this.db.prepare(`
488
580
  INSERT INTO memory_atlas_projection_state(project_id,projection_name,status,last_rebuild_at,last_error,metadata_json)
@@ -547,16 +639,22 @@ export class MemoryAtlasStore {
547
639
  `).all(...params);
548
640
  }
549
641
  episodeRows(projectId, episodeIds) {
550
- const bounded = Array.from(new Set(episodeIds)).slice(0, 500);
551
- if (!bounded.length)
552
- return [];
553
- return this.db.prepare(`
554
- SELECT d.*, COALESCE(a.activation, 0) AS activation
555
- FROM memory_atlas_documents d LEFT JOIN memory_atlas_activation a
556
- ON a.project_id=d.project_id AND a.node_id=d.node_id
557
- WHERE d.project_id=? AND d.node_id IN (${bounded.map(() => '?').join(',')})
558
- AND d.node_type='episode' AND d.status NOT IN ('rejected','archived')
559
- `).all(projectId, ...bounded.map((id) => `episode:${id}`));
642
+ const ids = Array.from(new Set(episodeIds));
643
+ const rows = [];
644
+ for (let index = 0; index < ids.length; index += 400) {
645
+ const chunk = ids.slice(index, index + 400);
646
+ if (!chunk.length)
647
+ continue;
648
+ rows.push(...this.db.prepare(`
649
+ SELECT d.*, COALESCE(a.activation, 0) AS activation
650
+ FROM memory_atlas_documents d LEFT JOIN memory_atlas_activation a
651
+ ON a.project_id=d.project_id AND a.node_id=d.node_id
652
+ WHERE d.project_id=? AND d.node_id IN (${chunk.map(() => '?').join(',')})
653
+ AND d.node_type='episode' AND d.status NOT IN ('rejected','archived')
654
+ ORDER BY d.occurred_at DESC, d.support_count DESC, d.node_id ASC
655
+ `).all(projectId, ...chunk.map((id) => `episode:${id}`)));
656
+ }
657
+ return rows;
560
658
  }
561
659
  cardFromEpisodeRow(row, edges, projectId) {
562
660
  const metadata = parseMetadata(row.metadata_json);
@@ -568,6 +666,7 @@ export class MemoryAtlasStore {
568
666
  });
569
667
  const issueHints = stringArray(metadata.issueHints);
570
668
  const topicHints = stringArray(metadata.topicHints);
669
+ const matchedTopicPaths = matchedFacets.filter((facet) => facet.type === 'topic').map((facet) => facet.value);
571
670
  return {
572
671
  canonicalId: row.node_id,
573
672
  nodeType: 'episode',
@@ -575,7 +674,7 @@ export class MemoryAtlasStore {
575
674
  oneLineSummary: row.summary || undefined,
576
675
  matchedFacets,
577
676
  matchedPaths,
578
- parentTopics: topicHints.length ? topicHints : row.topic_path ? [row.topic_path] : [],
677
+ parentTopics: matchedTopicPaths.length ? matchedTopicPaths : topicHints.length ? topicHints : row.topic_path ? [row.topic_path] : [],
579
678
  issueType: issueHints[0],
580
679
  eventKind: optionalMetadataString(metadata.eventKind),
581
680
  localDate: optionalMetadataString(metadata.localDate) ?? (row.occurred_at ? new Date(row.occurred_at).toISOString().slice(0, 10) : undefined),
@@ -629,6 +728,8 @@ catch {
629
728
  } }
630
729
  function escapeLike(value) { return value.replace(/[\\%_]/g, '\\$&'); }
631
730
  function nodeId(type, id, projectId) {
731
+ if (type === 'entity')
732
+ return id.startsWith('facet:') ? `entity:${projectId}:${id}` : `entity:${id}`;
632
733
  if (['topic', 'time', 'issue', 'session', 'thread', 'memoryKind', 'actionKind'].includes(type))
633
734
  return `${type}:${projectId}:${id}`;
634
735
  return `${type}:${id}`;
@@ -636,8 +737,18 @@ function nodeId(type, id, projectId) {
636
737
  function timeNodeId(projectId, occurredAt) {
637
738
  return `time:${projectId}:${new Date(occurredAt).getUTCFullYear()}`;
638
739
  }
740
+ function scopedEntityNodeId(db, value, projectId) {
741
+ if (!value.startsWith('entity:') || value.startsWith(`entity:${projectId}:`))
742
+ return value;
743
+ const id = value.slice('entity:'.length);
744
+ if (!id.startsWith('facet:'))
745
+ return value;
746
+ const scoped = `entity:${projectId}:${id}`;
747
+ const row = db.prepare(`SELECT 1 FROM memory_atlas_documents WHERE project_id=? AND node_id=?`).get(projectId, scoped);
748
+ return row ? scoped : value;
749
+ }
639
750
  function parseNodeId(value, projectId) {
640
- for (const type of ['topic', 'time', 'issue', 'session', 'thread', 'memoryKind', 'actionKind']) {
751
+ for (const type of ['topic', 'time', 'issue', 'entity', 'session', 'thread', 'memoryKind', 'actionKind']) {
641
752
  const prefix = `${type}:${projectId}:`;
642
753
  if (value.startsWith(prefix))
643
754
  return { type, id: value.slice(prefix.length) };
@@ -662,6 +773,12 @@ function stringArray(value) {
662
773
  function optionalMetadataString(value) {
663
774
  return typeof value === 'string' && value ? value : undefined;
664
775
  }
776
+ function chunked(values, size) {
777
+ const chunks = [];
778
+ for (let index = 0; index < values.length; index += size)
779
+ chunks.push(values.slice(index, index + size));
780
+ return chunks;
781
+ }
665
782
  function facetFromEdge(edge, projectId) {
666
783
  return {
667
784
  type: edge.target_type,
@@ -686,6 +803,29 @@ function facetLabel(type, value) {
686
803
  }
687
804
  return value;
688
805
  }
806
+ function groupFacetMatches(matches) {
807
+ const groups = new Map();
808
+ for (const match of matches) {
809
+ const targetSlug = equivalentTargetSlug(match.facet);
810
+ const key = targetSlug
811
+ ? `target:${targetSlug}`
812
+ : match.facet.type === 'actionKind' || match.facet.type === 'memoryKind'
813
+ ? `${match.facet.type}:${match.facet.relation || ''}`
814
+ : `${match.facet.type}:${match.facet.value}:${match.facet.relation || ''}`;
815
+ const group = groups.get(key) ?? [];
816
+ group.push(match);
817
+ groups.set(key, group);
818
+ }
819
+ return Array.from(groups.values());
820
+ }
821
+ function equivalentTargetSlug(facet) {
822
+ if (facet.type === 'entity' && facet.value.startsWith('facet:'))
823
+ return facet.value.slice('facet:'.length);
824
+ if (facet.type !== 'topic')
825
+ return undefined;
826
+ const match = facet.value.match(/^PROJECT\/[^/]+\/([^/]+)$/u);
827
+ return match?.[1];
828
+ }
689
829
  function intersectSets(sets) {
690
830
  if (!sets.length)
691
831
  return new Set();
@@ -697,12 +837,50 @@ function intersectSets(sets) {
697
837
  }
698
838
  return result;
699
839
  }
840
+ function cardRank(card) {
841
+ return scoreCard(card) + Math.min(card.evidenceTotal || 0, 10) * 0.01;
842
+ }
700
843
  function scoreCard(card) {
701
- const issuePriority = card.issueType === 'memory-context-blackbox' ? 4
702
- : card.issueType === 'graph-runtime-blackbox' ? 2
703
- : card.issueType === 'atlas-readability' ? 1
704
- : 0;
705
- return card.matchedFacets.length * 10 + issuePriority + card.matchedPaths.reduce((sum, path) => sum + path.confidence, 0) + (card.localDate ? 0.1 : 0);
844
+ return card.matchedFacets.length * 10 + card.matchedPaths.reduce((sum, path) => sum + path.confidence, 0) + (card.localDate ? 0.1 : 0);
845
+ }
846
+ function dedupeCardsByEvidence(cards) {
847
+ const bySignature = new Map();
848
+ const out = [];
849
+ for (const card of cards) {
850
+ const signature = evidenceSignature(card);
851
+ if (!signature) {
852
+ out.push(card);
853
+ continue;
854
+ }
855
+ const kept = bySignature.get(signature);
856
+ if (!kept) {
857
+ bySignature.set(signature, card);
858
+ out.push(card);
859
+ continue;
860
+ }
861
+ kept.relatedButNotSelected = mergeRelatedCards(kept.relatedButNotSelected ?? [], [{
862
+ canonicalId: card.canonicalId,
863
+ displayTitle: card.displayTitle,
864
+ reason: 'same primary raw evidence',
865
+ }]);
866
+ }
867
+ return out;
868
+ }
869
+ function evidenceSignature(card) {
870
+ const primary = card.sourceLocator?.eventId || card.evidenceEventIds[0];
871
+ if (!primary)
872
+ return undefined;
873
+ const facets = (type) => card.matchedFacets
874
+ .filter((facet) => facet.type === type)
875
+ .map((facet) => facet.value)
876
+ .sort()
877
+ .join(',');
878
+ return [
879
+ primary,
880
+ card.localDate || '',
881
+ facets('entity'),
882
+ facets('actionKind'),
883
+ ].join('|');
706
884
  }
707
885
  function mergeRelatedCards(...groups) {
708
886
  const merged = new Map();
@@ -0,0 +1,10 @@
1
+ export interface ActionKindRule {
2
+ kind: string;
3
+ label: string;
4
+ verb: string;
5
+ pattern: RegExp;
6
+ }
7
+ export declare const ACTION_KIND_RULES: ActionKindRule[];
8
+ export declare function inferActionKinds(text: string): string[];
9
+ export declare function inferFirstActionKind(text: string): ActionKindRule | undefined;
10
+ //# sourceMappingURL=ActionKindRegistry.d.ts.map
@@ -0,0 +1,18 @@
1
+ export const ACTION_KIND_RULES = [
2
+ { kind: 'started', label: '启动', verb: '启动', pattern: /启动|start|started|launch|launched|boot/i },
3
+ { kind: 'installed', label: '安装', verb: '安装', pattern: /安装|install|installed|setup/i },
4
+ { kind: 'configured', label: '配置', verb: '配置', pattern: /配置|config|configured|设置|修改配置|修改/i },
5
+ { kind: 'restarted', label: '重启', verb: '重启', pattern: /重启|restart|restarted/i },
6
+ { kind: 'stopped', label: '停止', verb: '停止', pattern: /停止|stop|stopped/i },
7
+ { kind: 'operated', label: '操作', verb: '操作', pattern: /操作|处理|执行|运行|run|ran/i },
8
+ { kind: 'implemented', label: '实现', verb: '实现', pattern: /修复|fixed|implemented|实现|提交|升级|upgrade|合并|发布/i },
9
+ { kind: 'reviewed', label: '审查', verb: '审查', pattern: /review|审查|检查/i },
10
+ { kind: 'decided', label: '决定', verb: '决定', pattern: /决定|decided|方案|策略|结论/i },
11
+ { kind: 'debugged', label: '排查', verb: '排查', pattern: /debug|排查|卡死|locked|zombie|报错|错误/i },
12
+ ];
13
+ export function inferActionKinds(text) {
14
+ return ACTION_KIND_RULES.filter((rule) => rule.pattern.test(text)).map((rule) => rule.kind);
15
+ }
16
+ export function inferFirstActionKind(text) {
17
+ return ACTION_KIND_RULES.find((rule) => rule.pattern.test(text));
18
+ }
@@ -0,0 +1,13 @@
1
+ export interface EntityCue {
2
+ label: string;
3
+ id: string;
4
+ }
5
+ export interface OperationalActionCue {
6
+ kind: string;
7
+ label: string;
8
+ verb: string;
9
+ }
10
+ export declare function extractEntityCues(text: string, limit?: number): EntityCue[];
11
+ export declare function inferOperationalActionCue(text: string): OperationalActionCue | undefined;
12
+ export declare function normalizeEntityCueId(label: string): string;
13
+ //# sourceMappingURL=EntityCueExtractor.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { inferFirstActionKind } from './ActionKindRegistry.js';
2
+ const ENTITY_STOP_WORDS = new Set([
3
+ 'a',
4
+ 'an',
5
+ 'and',
6
+ 'are',
7
+ 'before',
8
+ 'bug',
9
+ 'context',
10
+ 'database',
11
+ 'debug',
12
+ 'exact',
13
+ 'graph',
14
+ 'install',
15
+ 'installed',
16
+ 'launch',
17
+ 'launched',
18
+ 'memory',
19
+ 'operation',
20
+ 'operations',
21
+ 'quote',
22
+ 'recall',
23
+ 'sourcecontext',
24
+ 'start',
25
+ 'started',
26
+ 'the',
27
+ 'what',
28
+ 'you',
29
+ ]);
30
+ export function extractEntityCues(text, limit = 8) {
31
+ const normalized = String(text || '').replace(/\s+/g, ' ').trim();
32
+ const labels = [];
33
+ collectEntityMatches(normalized, /(?:对|给|把|关于|有关|围绕)\s*([\p{L}][\p{L}\p{N}_-]{1,48})\s*(?:做过|做了|做|启动|安装|配置|修改|重启|停止|操作|处理|执行|有关|的|什么|哪些|吗)?/giu, labels);
34
+ collectEntityMatches(normalized, /(?:聊过|讨论过|记得|还记得|提到过)\s*([\p{L}][\p{L}\p{N}_-]{1,48})\s*(?:什么|哪些|吗|的)?/giu, labels);
35
+ collectEntityMatches(normalized, /(?:启动|安装|配置|修改|重启|停止|执行|运行|start|started|launch|launched|boot|install|installed|setup|configure|configured|restart|stop|run|ran)\s*(?:本机安装的|本地的|the\s+)?([\p{L}][\p{L}\p{N}_-]{1,48})/giu, labels);
36
+ collectEntityMatches(normalized, /\b([A-Z][A-Za-z0-9_-]{1,48})\b/g, labels);
37
+ const seen = new Set();
38
+ const cues = [];
39
+ for (const label of labels.map(cleanEntityLabel).filter(Boolean)) {
40
+ const id = normalizeEntityCueId(label);
41
+ if (!id || ENTITY_STOP_WORDS.has(id) || isLikelyConceptLabel(label) || seen.has(id))
42
+ continue;
43
+ seen.add(id);
44
+ cues.push({ label, id });
45
+ if (cues.length >= limit)
46
+ break;
47
+ }
48
+ return cues;
49
+ }
50
+ export function inferOperationalActionCue(text) {
51
+ const rule = inferFirstActionKind(text);
52
+ return rule ? { kind: rule.kind, label: rule.label, verb: rule.verb } : undefined;
53
+ }
54
+ export function normalizeEntityCueId(label) {
55
+ return String(label || '')
56
+ .trim()
57
+ .toLocaleLowerCase()
58
+ .replace(/['"“”‘’`]/g, '')
59
+ .replace(/[^\p{L}\p{N}]+/gu, '-')
60
+ .replace(/^-|-$/g, '');
61
+ }
62
+ function collectEntityMatches(text, regex, out) {
63
+ for (const match of text.matchAll(regex)) {
64
+ if (match[1])
65
+ out.push(match[1]);
66
+ }
67
+ }
68
+ function cleanEntityLabel(label) {
69
+ return String(label || '')
70
+ .replace(/^(我|你|我们)?(?:之前)?让你(?:对|给|把)?/u, '')
71
+ .replace(/^(我们|你|我)?(?:之前)?聊过的?/u, '')
72
+ .replace(/(聊过|讨论过|记得|还记得|提到过).*$/u, '')
73
+ .replace(/^(本机安装的|本地的|the\s+)/i, '')
74
+ .replace(/(做过|做了|启动|安装|配置|修改|重启|停止|操作|处理|执行)(什么|哪些|吗|么|的)?$/u, '')
75
+ .replace(/(什么|哪些|吗|么|的)$/u, '')
76
+ .replace(/[,。?!、;:,.?!;:()[\]{}<>]/g, '')
77
+ .trim();
78
+ }
79
+ function isLikelyConceptLabel(label) {
80
+ return /(黑盒|问题|方案|策略|计划|原话|上下文|记忆)$/u.test(label);
81
+ }
@@ -227,7 +227,7 @@ Hermes has no automatic observation path in this integration. Use `cogmem_episod
227
227
 
228
228
  Use `cogmem_topic_operate` with actor `user_explicit` only for explicit user naming/organization instructions. Model suggestions use `model_candidate` and remain reviewable; alias collisions fail closed. Use `cogmem_episode_repair` for split/merge/move/reclassify/requeue work so receipts, stale candidates, cross-references, Dream jobs, and audit records stay synchronized. Do not hand-edit the database.
229
229
 
230
- Use `cogmem memory map --project hermes --json` or MCP `cogmem_memory_map` to inspect Memory Binding and Graph Recall counters. Bindings attach high-value user raw events to stable topic/entity paths, fuse same-claim evidence into claim-key clusters, and create graph anchors for source drill-down only; they are not verified facts, user preferences, or instructions. Correction bindings expose review flags and correction edges. If `cogmem memory tick --project hermes --json` suggests `bind_raw_events`, run `cogmem memory bind --project hermes --json`; it scans the historical ledger by cursor and can resume with `--since <globalSeq>`. For old-discussion questions, use `cogmem memory recall --intent historical_discussion ...` or `cogmem_graph_explore`; in 3.7.0 cards carry `canonicalId`, `matchedFacets`, `matchedPaths`, `relatedButNotSelected`, and `sourceLocator`. Answer from the selected canonical card, not from a side card. Re-run `cogmem connect hermes --workspace . --auto --force --json` after upgrades to patch existing MCP allow-lists with new Cogmem tools. Follow only `nextCommands` for unattended agent work; operator/host actions are in `nextSteps` with `safeForAutomation=false`.
230
+ Use `cogmem memory map --project hermes --json` or MCP `cogmem_memory_map` to inspect Memory Binding and Graph Recall counters. Bindings attach high-value user raw events to stable topic/entity paths, fuse same-claim evidence into claim-key clusters, and create graph anchors for source drill-down only; they are not verified facts, user preferences, or instructions. Correction bindings expose review flags and correction edges. If `cogmem memory tick --project hermes --json` suggests `bind_raw_events`, run `cogmem memory bind --project hermes --json`; it scans the historical ledger by cursor and can resume with `--since <globalSeq>`. For old-discussion questions, use `cogmem memory recall --intent historical_discussion ...` or `cogmem_graph_explore`; in 3.7.2 cards carry `canonicalId`, `matchedFacets`, `matchedPaths`, `relatedButNotSelected`, and `sourceLocator`. Answer from the selected canonical card, not from a side card. Re-run `cogmem connect hermes --workspace . --auto --force --json` after upgrades to patch existing MCP allow-lists with new Cogmem tools. Follow only `nextCommands` for unattended agent work; operator/host actions are in `nextSteps` with `safeForAutomation=false`.
231
231
 
232
232
  Dream correction records require explicit user clarification; assistant self-correction and questions containing `是不是` are not user-owned contradictions. Invalid provider output is a rejected diagnostic. Maintenance ticks supersede `needs_confirmation` entries older than the default 30-day TTL and retain their evidence rows.
233
233
 
@@ -109,7 +109,7 @@ Dream stores explicit user clarification as organizational correction evidence r
109
109
 
110
110
  After upgrades, reload MCP. Rerun `cogmem connect hermes --workspace . --auto --force --json` when MCP wiring, allow-listed tools, or the installed skill bundle changed. Follow only JSON `nextCommands` for unattended agent work; operator/host actions are in `nextSteps` with `safeForAutomation=false`.
111
111
 
112
- Cogmem 3.7.0 exposes read-only/idempotent Memory Atlas query tools plus explicit `cogmem_graph_touch`, installs from npm by default, prevents empty imported episodes from blocking Dream, and adds an agent-safe operations protocol. Use `memory plan` for the next safe command, grouped `memory candidates --json` for queue state, `historical_discussion` recall for old-discussion questions, and returned `sourceLocator` commands for exact evidence. Only run `dream_tick` when it appears in `memory plan.nextActions`; `raw_dream_ledger_lag` in `nonBlocking` has `resolvableByDreamTick:false` and is not fixed by looping `dream tick`. Use explore for broad memory inventory/history, search and node for a known concept, path/neighbors for relations, timeline for ordered reconstruction, and normal recall for a direct fact. Query facets combine the user's actual project, day/month/year, topic, issue, entity/target, session/thread, memory-kind, action-kind, and keyword conditions like table filters, so cold memory can be revived without requiring an entity-time-action tuple. Prefer returned `cards[]` for historical discussion: `canonicalId` dedupes the episode, `matchedFacets` explains why it matched, `relatedButNotSelected` is only side context, and `sourceLocator` opens raw evidence. Touch only nodes actually used, and follow returned event IDs or `sourceLocator` commands before quoting exact wording.
112
+ Cogmem 3.7.2 exposes read-only/idempotent Memory Atlas query tools plus explicit `cogmem_graph_touch`, installs from npm by default, prevents empty imported episodes from blocking Dream, and adds an agent-safe operations protocol. Use `memory plan` for the next safe command, grouped `memory candidates --json` for queue state, `historical_discussion` recall for old-discussion questions, and returned `sourceLocator` commands for exact evidence. Only run `dream_tick` when it appears in `memory plan.nextActions`; `raw_dream_ledger_lag` in `nonBlocking` has `resolvableByDreamTick:false` and is not fixed by looping `dream tick`. Use explore for broad memory inventory/history, search and node for a known concept, path/neighbors for relations, timeline for ordered reconstruction, and normal recall for a direct fact. Query facets combine the user's actual project, day/month/year, topic, issue, entity/target, session/thread, memory-kind, action-kind, and keyword conditions like table filters, so cold memory can be revived without requiring an entity-time-action tuple. Unicode entity cues are supported, but alias merges still require governed evidence. Prefer returned `cards[]` for historical discussion: `canonicalId` dedupes the episode, `matchedFacets` explains why it matched, `relatedButNotSelected` is only side context, `relaxationTrace` marks day/month/year or issue/topic fallback, and `sourceLocator` opens raw evidence. Touch only nodes actually used, and follow returned event IDs or `sourceLocator` commands before quoting exact wording.
113
113
 
114
114
  ## Runtime
115
115
 
@@ -114,7 +114,7 @@ timeout_ms = 60000
114
114
 
115
115
  ## Migrate Existing Hermes Memory
116
116
 
117
- Upgrade a 3.5.2 database, an existing 3.6.x database, or a pre-release schema-25 test database to the current 3.7.0 schema/projection set in one backed-up command:
117
+ Upgrade a 3.5.2 database, an existing 3.6.x database, or a pre-release schema-25 test database to the current 3.7.2 schema/projection set in one backed-up command:
118
118
 
119
119
  ```bash
120
120
  cogmem migrate --yes --backup --json
@@ -214,9 +214,9 @@ Choose the graph tool from the question shape:
214
214
  - Direct factual question: `cogmem_recall`.
215
215
  - Exact wording: follow an event ID with `cogmem memory show`.
216
216
 
217
- Atlas combines whatever conditions the user supplies like table filters. Do not require entity + time + action. Project, day/month/year, topic, issue, entity/target, session/thread, memory kind, action kind, and ordinary cues may revive cold nodes together. Activation is visibility, not truth. Atlas summaries are hints and never replace raw evidence.
217
+ Atlas combines whatever conditions the user supplies like table filters. Do not require entity + time + action. Project, day/month/year, topic, issue, entity/target, session/thread, memory kind, action kind, and ordinary cues may revive cold nodes together. Entity cue extraction accepts Unicode letter/number names, but it is not full NER and must not be treated as proof that two aliases are the same person/tool. Activation is visibility, not truth. Atlas summaries are hints and never replace raw evidence.
218
218
 
219
- In 3.7.0, `cogmem_graph_search`, `cogmem_graph_explore`, and historical recall can return canonical `cards[]`. Prefer cards over raw node labels for past-discussion questions. A card's `canonicalId` is the single episode identity; `matchedFacets` explains time/topic/issue/entity matches; `matchedPaths` explains how the card was reached; `relatedButNotSelected` is context only and must not replace the selected answer; `sourceLocator.command` is the command to inspect exact original text. If `relaxationTrace` exists, say the answer is a relaxed nearby match, not an exact hit.
219
+ In 3.7.2, `cogmem_graph_search`, `cogmem_graph_explore`, and historical recall can return canonical `cards[]`. Prefer cards over raw node labels for past-discussion questions. A card's `canonicalId` is the single episode identity; `matchedFacets` explains time/topic/issue/entity matches; `matchedPaths` explains how the card was reached; `relatedButNotSelected` is context only and must not replace the selected answer; `sourceLocator.command` is the command to inspect exact original text. If `relaxationTrace` exists, say the answer is a relaxed match, not an exact hit; supported relaxations include day → month → year and issue → parent topic.
220
220
 
221
221
  When the prompt does not contain enough injected Cogmem context, do not search legacy memory files first. Ask Cogmem directly:
222
222
 
@@ -245,6 +245,13 @@ For “did we discuss this before?”, “几个月前是不是聊过…”, “
245
245
  cogmem memory recall --query "<past discussion question>" --intent historical_discussion --project hermes --agent hermes --json
246
246
  ```
247
247
 
248
+ For action-history questions such as “what did I ask you to do to <entity>?”, “启动 <tool>”, or “对 <entity> 做过什么操作”, use `action_history` or Atlas timeline. The entity is whatever project/tool/person/service the user mentions in memory; it is not limited to Hermes itself:
249
+
250
+ ```bash
251
+ cogmem memory recall --query "我之前让你对 <实体或工具名> 做过什么" --intent action_history --project hermes --agent hermes --json
252
+ cogmem memory graph-timeline --project hermes --query "对 <实体或工具名> 做过什么操作" --include-evidence --json
253
+ ```
254
+
248
255
  If the answer depends on exact wording or nearby context, run the returned `sourceLocator` or use a Raw Ledger cursor:
249
256
 
250
257
  ```bash
@@ -1,4 +1,4 @@
1
- # Cogmem 3.7.0 Operations Reference for Hermes
1
+ # Cogmem 3.7.2 Operations Reference for Hermes
2
2
 
3
3
  Read this file when installing, upgrading, importing, repairing, or operating Cogmem. `SKILL.md` contains the decision rules; this file records the operational commands.
4
4
 
@@ -76,7 +76,7 @@ cogmem doctor
76
76
  cogmem connect hermes --workspace . --auto --force --json
77
77
  ```
78
78
 
79
- The backed-up command upgrades 3.5.2 schema 24, an existing 3.6.x database, or a pre-release schema-25 test database to the current 3.7.0 schema/projection state in one run and preserves Raw Ledger evidence. `--dry-run` is read-only and does not create `_schema_migrations`. Reload MCP after reconnecting.
79
+ The backed-up command upgrades 3.5.2 schema 24, an existing 3.6.x database, or a pre-release schema-25 test database to the current 3.7.2 schema/projection state in one run and preserves Raw Ledger evidence. `--dry-run` is read-only and does not create `_schema_migrations`. Reload MCP after reconnecting.
80
80
 
81
81
  ## Import Hermes memory
82
82
 
@@ -149,22 +149,23 @@ cogmem memory list --project hermes --since <globalSeq> --order asc --json
149
149
 
150
150
  JSON uses `cogmem.cli.v1`: object fields are top-level, arrays use `items`, and queue counters remain top-level. `vectors: 0` is not a recall failure; inspect `vectorState`.
151
151
 
152
- Use `historical_discussion` for “did we discuss this before?”, “几个月前是不是聊过…”, “记忆黑盒”, and missing MCP memory cases. Raw list rows include `sourceLocator`; run the locator before quoting exact words or saying the event cannot be found. Read-only plan/status/candidates use a lightweight SQLite path and should not require stopping the MCP server.
152
+ Use `historical_discussion` for “did we discuss this before?”, “几个月前是不是聊过…”, “记忆黑盒”, and missing MCP memory cases. Use `action_history` for “what did I ask you to do to <entity>?”, “启动 <tool>”, or “对 <entity> 做过什么操作”; the entity is the project/tool/person/service named in memory, not necessarily Hermes. Raw list rows include `sourceLocator`; run the locator before quoting exact words or saying the event cannot be found. Read-only plan/status/candidates use a lightweight SQLite path and should not require stopping the MCP server.
153
153
 
154
154
  ## Memory Atlas as composable filters
155
155
 
156
- Atlas uses any available project, day/month/year, topic, issue, entity/target, session/thread, memory-kind, action-kind, and text facets together. It does not require an entity + time + operation tuple. A canonical episode appears once even if it is reached through several facets.
156
+ Atlas uses any available project, day/month/year, topic, issue, entity/target, session/thread, memory-kind, action-kind, and text facets together. Unicode letter/number entity cues are supported, but Atlas does not automatically prove aliases or merge identities. It does not require an entity + time + operation tuple. A canonical episode appears once even if it is reached through several facets.
157
157
 
158
158
  ```bash
159
159
  cogmem memory graph --project hermes --json
160
- cogmem memory graph-search --project hermes --query "Hermes" --json
161
- cogmem memory graph-explore --project hermes --query "2025 年 Hermes 的决策" --now 1782057600000 --evidence-limit 2 --json
160
+ cogmem memory graph-search --project hermes --query "<实体或工具名>" --json
161
+ cogmem memory graph-explore --project hermes --query "2025 年 <实体或工具名> 的决策" --now 1782057600000 --evidence-limit 2 --json
162
162
  cogmem memory graph-explore --project hermes --query "6月6号关于记忆黑盒聊过什么" --json
163
163
  cogmem memory graph-explore --project hermes --query "记忆黑盒后来有没有继续讨论" --json
164
164
  cogmem memory graph-node --project hermes --id <node-id> --include-evidence --evidence-limit 4 --json
165
165
  cogmem memory graph-neighbors --project hermes --id <node-id> --hops 2 --json
166
166
  cogmem memory graph-path --project hermes --from <node-id> --to <node-id> --json
167
- cogmem memory graph-timeline --project hermes --query "去年与 Hermes 有关的修复" --now 1782057600000 --evidence-limit 4 --json
167
+ cogmem memory graph-timeline --project hermes --query "去年与 <实体或工具名> 有关的修复" --now 1782057600000 --evidence-limit 4 --json
168
+ cogmem memory graph-timeline --project hermes --query "对 <实体或工具名> 做过什么操作" --include-evidence --json
168
169
  ```
169
170
 
170
171
  Use MCP by question shape:
@@ -176,7 +177,7 @@ Use MCP by question shape:
176
177
  - Direct fact: `cogmem_recall`.
177
178
  - Exact source: follow `evidenceEventIds` with `memory show`.
178
179
 
179
- Graph reads are pure and declared read-only/idempotent. Call `cogmem_graph_touch` only after using selected nodes. Overview display alone must not change future ranking. Search/explore may return `cards[]` for canonical episodes. Use `cards[].displayTitle` for UI/readability, `oneLineSummary` as a hint, `matchedFacets` and `matchedPaths` to explain why it matched, and `canonicalId` to dedupe. `relatedButNotSelected` is context only; do not substitute it as the answer. If `relaxationTrace` exists, say the match was relaxed. `evidenceTotal` is all known evidence; `evidenceReturned` is the bounded payload. Search/explore evidence includes `sourceLocator.command` and `sourceLocator.contextCommand`; use those locators before treating an Atlas summary as evidence.
180
+ Graph reads are pure and declared read-only/idempotent. Call `cogmem_graph_touch` only after using selected nodes. Overview display alone must not change future ranking. Search/explore may return `cards[]` for canonical episodes. Use `cards[].displayTitle` for UI/readability, `oneLineSummary` as a hint, `matchedFacets` and `matchedPaths` to explain why it matched, and `canonicalId` to dedupe. `relatedButNotSelected` is context only; do not substitute it as the answer. If `relaxationTrace` exists, say the match was relaxed; supported relaxations are day → month → year and issue → parent topic. `evidenceTotal` is all known evidence; `evidenceReturned` is the bounded payload. Search/explore evidence includes `sourceLocator.command` and `sourceLocator.contextCommand`; use those locators before treating an Atlas summary as evidence.
180
181
 
181
182
  ## Candidate governance and review
182
183
 
@@ -185,7 +185,7 @@ Recall behavior:
185
185
  - If `cogmem memory tick --project openclaw --json` suggests `bind_raw_events`, run `cogmem memory bind --project openclaw --json` to backfill imported or adapter-written raw user events into the binding graph.
186
186
  - For “did we discuss this before?”, “几个月前是不是聊过…”, “记忆黑盒”, or missing injection cases, run `cogmem memory recall --query "<past discussion question>" --intent historical_discussion --project openclaw --agent openclaw --json` before claiming no memory.
187
187
  - For ledger audits or resumable checks, use `cogmem memory list --project openclaw --since <globalSeq> --order asc --json`; list rows and Atlas search/explore evidence include `sourceLocator` commands.
188
- - For old-discussion or "do you remember" questions, prefer `cogmem memory recall --intent historical_discussion ...` or `cogmem memory graph-explore ... --json`. In 3.7.0, cards carry `canonicalId`, `matchedFacets`, `matchedPaths`, `relatedButNotSelected`, and `sourceLocator`; answer from the selected canonical card, not from a related-but-not-selected side card.
188
+ - For old-discussion or "do you remember" questions, prefer `cogmem memory recall --intent historical_discussion ...` or `cogmem memory graph-explore ... --json`. In 3.7.2, cards carry `canonicalId`, `matchedFacets`, `matchedPaths`, `relatedButNotSelected`, and `sourceLocator`; answer from the selected canonical card, not from a related-but-not-selected side card.
189
189
 
190
190
  Installing the workspace skill makes the kernel procedure discoverable to OpenClaw agents. Installing the local auto wrapper makes future turns call the memory kernel automatically:
191
191
 
@@ -84,7 +84,7 @@ cogmem memory tick --project openclaw --json
84
84
  cogmem memory bind --project openclaw --json
85
85
  ```
86
86
 
87
- Cogmem 3.7.0 keeps npm as the default install/update channel and upgrades Memory Atlas into a multi-dimensional navigation graph. Use `memory plan` for the next safe command, grouped `memory candidates --json` for queue state, `historical_discussion` recall for old-discussion questions, and returned `sourceLocator` commands for exact evidence. Only run `dream_tick` when it appears in `memory plan.nextActions`; `raw_dream_ledger_lag` in `nonBlocking` has `resolvableByDreamTick:false` and is not fixed by looping `dream tick`. The auto plugin uses one shared bridge/kernel lifecycle for graph exploration, evidence-bearing node/timeline drill-down, and recall, so OpenClaw does not need MCP for broad inventory/history questions. Atlas combines the query's actual project, day/month/year, topic, issue, entity/target, session/thread, memory-kind, action-kind, and keyword conditions like table filters; no fixed entity-time-action tuple is required. Prefer returned `cards[]` for historical discussion: `canonicalId` dedupes the episode, `matchedFacets` explains why it matched, `relatedButNotSelected` is only side context, and `sourceLocator` opens the raw evidence.
87
+ Cogmem 3.7.2 keeps npm as the default install/update channel and upgrades Memory Atlas into a multi-dimensional navigation graph. Use `memory plan` for the next safe command, grouped `memory candidates --json` for queue state, `historical_discussion` recall for old-discussion questions, and returned `sourceLocator` commands for exact evidence. Only run `dream_tick` when it appears in `memory plan.nextActions`; `raw_dream_ledger_lag` in `nonBlocking` has `resolvableByDreamTick:false` and is not fixed by looping `dream tick`. The auto plugin uses one shared bridge/kernel lifecycle for graph exploration, evidence-bearing node/timeline drill-down, and recall, so OpenClaw does not need MCP for broad inventory/history questions. Atlas combines the query's actual project, day/month/year, topic, issue, entity/target, session/thread, memory-kind, action-kind, and keyword conditions like table filters; no fixed entity-time-action tuple is required. Unicode entity cues are supported, but alias merges still require governed evidence. Prefer returned `cards[]` for historical discussion: `canonicalId` dedupes the episode, `matchedFacets` explains why it matched, `relatedButNotSelected` is only side context, `relaxationTrace` marks day/month/year or issue/topic fallback, and `sourceLocator` opens the raw evidence.
88
88
 
89
89
  ```bash
90
90
  cogmem memory plan --project openclaw --json
@@ -230,7 +230,7 @@ To make future OpenClaw turns automatically recall and record memory, run:
230
230
  cogmem connect openclaw --workspace . --auto --force
231
231
  ```
232
232
 
233
- `--auto` installs `<workspace>/extensions/cogmem-auto-memory/`, patches OpenClaw `plugins.load.paths`, and enables a local plugin wrapper with `before_prompt_build` and `agent_end` hooks. The wrapper calls `KernelAgentMemoryBackend` through the public `cogmem` API via a Bun bridge; core still does not import OpenClaw. Plugin 0.7.0 uses singleton queue/spawn locks, stale-lock and stale-processing recovery, and bounded remember batches so queued `agent_end` recording does not hold SQLite open longer than necessary.
233
+ `--auto` installs `<workspace>/extensions/cogmem-auto-memory/`, patches OpenClaw `plugins.load.paths`, and enables a local plugin wrapper with `before_prompt_build` and `agent_end` hooks. The wrapper calls `KernelAgentMemoryBackend` through the public `cogmem` API via a Bun bridge; core still does not import OpenClaw. Plugin 0.7.1 uses singleton queue/spawn locks, stale-lock and stale-processing recovery, bounded remember batches, and untrusted-memory serialization so queued `agent_end` recording does not hold SQLite open longer than necessary and recalled history cannot create `COGMEM_*` prompt blocks.
234
234
 
235
235
  The wrapper does not rewrite OpenClaw's native prompt, tool instructions, skills, or conversation order. It only prepends Cogmem-owned blocks:
236
236