cogmem 3.6.5 → 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 (46) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/MEMORY_ATLAS.md +59 -12
  3. package/README.md +21 -13
  4. package/RELEASE_CHECKLIST.md +5 -4
  5. package/dist/agent/AgentMemoryBackend.d.ts +24 -3
  6. package/dist/agent/AgentMemoryBackend.js +285 -55
  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.d.ts +31 -0
  15. package/dist/atlas/EpisodeTitleGenerator.js +191 -0
  16. package/dist/atlas/FacetQueryPlanner.d.ts +33 -0
  17. package/dist/atlas/FacetQueryPlanner.js +229 -0
  18. package/dist/atlas/GraphCurator.d.ts +30 -0
  19. package/dist/atlas/GraphCurator.js +501 -0
  20. package/dist/atlas/MemoryAtlasIndexer.d.ts +15 -0
  21. package/dist/atlas/MemoryAtlasIndexer.js +55 -6
  22. package/dist/atlas/MemoryAtlasQueryCompiler.js +10 -7
  23. package/dist/atlas/MemoryAtlasService.d.ts +4 -1
  24. package/dist/atlas/MemoryAtlasService.js +174 -11
  25. package/dist/atlas/MemoryAtlasTypes.d.ts +75 -11
  26. package/dist/bin/memory.js +18 -5
  27. package/dist/factory.d.ts +18 -0
  28. package/dist/factory.js +41 -2
  29. package/dist/host/openclaw/AutoMemoryPluginInstaller.js +135 -39
  30. package/dist/mcp/server.js +1 -1
  31. package/dist/recall/RecallExplanation.d.ts +1 -1
  32. package/dist/store/MemoryAtlasStore.d.ts +12 -1
  33. package/dist/store/MemoryAtlasStore.js +353 -15
  34. package/dist/utils/ActionKindRegistry.d.ts +10 -0
  35. package/dist/utils/ActionKindRegistry.js +18 -0
  36. package/dist/utils/EntityCueExtractor.d.ts +13 -0
  37. package/dist/utils/EntityCueExtractor.js +81 -0
  38. package/examples/hermes-backend/AGENTS.md +3 -3
  39. package/examples/hermes-backend/README.md +1 -1
  40. package/examples/hermes-backend/SKILL.md +14 -5
  41. package/examples/hermes-backend/references/operations.md +12 -9
  42. package/examples/openclaw-backend/AGENTS.md +3 -2
  43. package/examples/openclaw-backend/README.md +3 -2
  44. package/examples/openclaw-backend/SKILL.md +31 -9
  45. package/examples/openclaw-backend/references/operations.md +29 -11
  46. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
+ export const MEMORY_ATLAS_PROJECTION_NAME = 'memory_atlas.v2';
3
+ export const MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION = '3.7.2';
2
4
  export class MemoryAtlasStore {
3
5
  db;
4
6
  constructor(db) {
@@ -21,12 +23,13 @@ export class MemoryAtlasStore {
21
23
  this.refreshFtsNode(input.id);
22
24
  }
23
25
  getNode(nodeId, projectId) {
26
+ const scopedNodeId = scopedEntityNodeId(this.db, nodeId, projectId);
24
27
  const row = this.db.prepare(`
25
28
  SELECT d.*, COALESCE(a.activation, 0) AS activation
26
29
  FROM memory_atlas_documents d LEFT JOIN memory_atlas_activation a
27
30
  ON a.project_id=d.project_id AND a.node_id=d.node_id
28
31
  WHERE d.node_id=? AND d.project_id=?
29
- `).get(nodeId, projectId);
32
+ `).get(scopedNodeId, projectId);
30
33
  return row ? mapNode(row, 0) : null;
31
34
  }
32
35
  listNodes(projectId, limit) {
@@ -82,12 +85,83 @@ export class MemoryAtlasStore {
82
85
  return mapNode(row, matches + Number(row.activation || 0));
83
86
  }).sort((a, b) => b.score - a.score);
84
87
  }
88
+ searchCanonicalEpisodeCards(projectId, plan, limit) {
89
+ const plannedFacets = plan.facets;
90
+ if (!plannedFacets.length)
91
+ return [];
92
+ const facetMatches = plannedFacets.map((facet) => ({ facet, rows: this.episodeEdgesForFacet(projectId, facet) }));
93
+ const facetGroups = groupFacetMatches(facetMatches);
94
+ if (plan.operator === 'intersection' && facetGroups.some((group) => group.every((match) => match.rows.length === 0)))
95
+ return [];
96
+ const candidateIds = plan.operator === 'intersection'
97
+ ? intersectSets(facetGroups.map((group) => new Set(group.flatMap((match) => match.rows.map((row) => row.source_id)))))
98
+ : new Set(facetMatches.flatMap((match) => match.rows.map((row) => row.source_id)));
99
+ if (!candidateIds.size)
100
+ return [];
101
+ const rows = this.episodeRows(projectId, Array.from(candidateIds));
102
+ const rankedCards = rows.map((row) => {
103
+ const candidateEdges = facetMatches.flatMap((match) => match.rows.filter((edge) => edge.source_id === row.source_id));
104
+ return this.cardFromEpisodeRow(row, candidateEdges, projectId);
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)));
107
+ const selectedIds = new Set(cards.map((card) => card.canonicalId));
108
+ for (const card of cards) {
109
+ card.relatedButNotSelected = mergeRelatedCards(card.relatedButNotSelected ?? [], this.relatedEpisodeCards(projectId, card.canonicalId, selectedIds, 4), this.topicRelatedCardsForPlan(projectId, plan, selectedIds, 4)).slice(0, 4);
110
+ }
111
+ return cards;
112
+ }
113
+ relatedEpisodeCards(projectId, canonicalId, selectedIds, limit) {
114
+ const parsed = parseNodeId(canonicalId, projectId);
115
+ if (!parsed || parsed.type !== 'episode')
116
+ return [];
117
+ const rows = this.db.prepare(`
118
+ SELECT e.relation_type, e.confidence, d.node_id, d.label, d.metadata_json
119
+ FROM memory_edges e
120
+ JOIN memory_atlas_documents d ON d.project_id=e.project_id AND d.node_id=CASE
121
+ WHEN e.source_type='episode' AND e.source_id=? THEN 'episode:' || e.target_id
122
+ ELSE 'episode:' || e.source_id
123
+ END
124
+ WHERE e.project_id=? AND e.status IN ('active','weak') AND e.relation_type IN ('SAME_ISSUE','FOLLOWS_UP','REFINES','CORRECTS','CONTRADICTS','SUPERSEDES','RELATED_TO')
125
+ AND ((e.source_type='episode' AND e.source_id=?) OR (e.target_type='episode' AND e.target_id=?))
126
+ ORDER BY CASE e.relation_type WHEN 'SAME_ISSUE' THEN 1 WHEN 'FOLLOWS_UP' THEN 2 WHEN 'REFINES' THEN 3 ELSE 4 END, e.confidence DESC
127
+ LIMIT ?
128
+ `).all(parsed.id, projectId, parsed.id, parsed.id, Math.max(1, Math.min(limit * 3, 30)));
129
+ const result = [];
130
+ for (const row of rows) {
131
+ if (selectedIds.has(row.node_id))
132
+ continue;
133
+ result.push({
134
+ canonicalId: row.node_id,
135
+ displayTitle: row.label,
136
+ reason: row.relation_type === 'RELATED_TO' ? 'related topic but different issue/date' : row.relation_type.toLowerCase(),
137
+ });
138
+ if (result.length >= limit)
139
+ break;
140
+ }
141
+ return result;
142
+ }
143
+ topicRelatedCardsForPlan(projectId, plan, selectedIds, limit) {
144
+ const topicFacets = plan.facets.filter((facet) => facet.type === 'topic');
145
+ if (!topicFacets.length)
146
+ return [];
147
+ const rows = topicFacets.flatMap((facet) => this.episodeEdgesForFacet(projectId, facet));
148
+ const candidateIds = Array.from(new Set(rows.map((row) => row.source_id))).filter((id) => !selectedIds.has(`episode:${id}`));
149
+ if (!candidateIds.length)
150
+ return [];
151
+ return this.episodeRows(projectId, candidateIds)
152
+ .map((row) => ({
153
+ canonicalId: row.node_id,
154
+ displayTitle: row.label,
155
+ reason: 'related topic but different issue/date',
156
+ }))
157
+ .slice(0, Math.max(1, Math.min(limit, 20)));
158
+ }
85
159
  resolveTargetNodeIds(projectId, query) {
86
160
  const normalizedQuery = normalizeLookup(query);
87
161
  const candidates = this.db.prepare(`
88
162
  SELECT node_id,node_type,source_id,label,topic_path,metadata_json,evidence_event_ids_json
89
163
  FROM memory_atlas_documents
90
- WHERE project_id=? AND node_type IN ('entity','topic','project') AND status NOT IN ('rejected','archived')
164
+ WHERE project_id=? AND node_type IN ('entity','topic','issue','project') AND status NOT IN ('rejected','archived')
91
165
  `).all(projectId);
92
166
  const seeds = [];
93
167
  const labels = [];
@@ -375,12 +449,34 @@ export class MemoryAtlasStore {
375
449
  }
376
450
  projectionNeedsRefresh(projectId) {
377
451
  const row = this.db.prepare(`
452
+ SELECT status, metadata_json FROM memory_atlas_projection_state
453
+ WHERE project_id=? AND projection_name=?
454
+ `).get(projectId, MEMORY_ATLAS_PROJECTION_NAME);
455
+ if (!row || row.status !== 'clean')
456
+ return true;
457
+ const metadata = parseMetadata(row.metadata_json);
458
+ if (metadata.projectionSchemaVersion !== MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION)
459
+ return true;
460
+ const legacyDirty = this.db.prepare(`
378
461
  SELECT status FROM memory_atlas_projection_state
379
- WHERE project_id=? AND projection_name='memory_atlas.v1'
462
+ WHERE project_id=? AND projection_name='memory_atlas.v1' AND status<>'clean'
380
463
  `).get(projectId);
381
- return !row || row.status !== 'clean';
464
+ return Boolean(legacyDirty);
382
465
  }
383
466
  markProjectionClean(projectId, metadata = {}, now = Date.now()) {
467
+ const enrichedMetadata = {
468
+ ...metadata,
469
+ projectionName: MEMORY_ATLAS_PROJECTION_NAME,
470
+ projectionSchemaVersion: MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION,
471
+ };
472
+ this.db.prepare(`
473
+ INSERT INTO memory_atlas_projection_state(
474
+ project_id, projection_name, cursor_value, status, last_rebuild_at, last_error, metadata_json
475
+ ) VALUES(?, ?, ?, 'clean', ?, NULL, ?)
476
+ ON CONFLICT(project_id, projection_name) DO UPDATE SET
477
+ cursor_value=excluded.cursor_value, status='clean', last_rebuild_at=excluded.last_rebuild_at,
478
+ last_error=NULL, metadata_json=excluded.metadata_json
479
+ `).run(projectId, MEMORY_ATLAS_PROJECTION_NAME, String(now), now, JSON.stringify(enrichedMetadata));
384
480
  this.db.prepare(`
385
481
  INSERT INTO memory_atlas_projection_state(
386
482
  project_id, projection_name, cursor_value, status, last_rebuild_at, last_error, metadata_json
@@ -388,17 +484,61 @@ export class MemoryAtlasStore {
388
484
  ON CONFLICT(project_id, projection_name) DO UPDATE SET
389
485
  cursor_value=excluded.cursor_value, status='clean', last_rebuild_at=excluded.last_rebuild_at,
390
486
  last_error=NULL, metadata_json=excluded.metadata_json
391
- `).run(projectId, String(now), now, JSON.stringify(metadata));
487
+ `).run(projectId, String(now), now, JSON.stringify({ compatibilityAliasFor: MEMORY_ATLAS_PROJECTION_NAME }));
488
+ }
489
+ markProjectionDirty(projectId, metadata = {}, now = Date.now()) {
490
+ this.db.prepare(`
491
+ INSERT INTO memory_atlas_projection_state(project_id,projection_name,cursor_value,status,last_rebuild_at,last_error,metadata_json)
492
+ VALUES(?, ?, NULL, 'dirty', ?, NULL, ?)
493
+ ON CONFLICT(project_id,projection_name) DO UPDATE SET
494
+ status='dirty', cursor_value=NULL, last_error=NULL, metadata_json=excluded.metadata_json
495
+ `).run(projectId, MEMORY_ATLAS_PROJECTION_NAME, now, JSON.stringify({
496
+ ...metadata,
497
+ projectionName: MEMORY_ATLAS_PROJECTION_NAME,
498
+ projectionSchemaVersion: MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION,
499
+ }));
500
+ this.db.prepare(`
501
+ INSERT INTO memory_atlas_projection_state(project_id,projection_name,cursor_value,status,last_rebuild_at,last_error,metadata_json)
502
+ VALUES(?, 'memory_atlas.v1', NULL, 'dirty', ?, NULL, ?)
503
+ ON CONFLICT(project_id,projection_name) DO UPDATE SET status='dirty', cursor_value=NULL, last_error=NULL, metadata_json=excluded.metadata_json
504
+ `).run(projectId, now, JSON.stringify({ compatibilityAliasFor: MEMORY_ATLAS_PROJECTION_NAME, dirtyBecause: 'atlas_v2_targeted_reindex' }));
505
+ }
506
+ aggregateFacetNodeSupport(projectId, now = Date.now()) {
507
+ const rows = this.db.prepare(`
508
+ SELECT target_type,target_id,evidence_event_ids_json
509
+ FROM memory_edges
510
+ WHERE project_id=? AND source_type='episode'
511
+ AND target_type IN ('topic','time','issue','entity','session','thread','memoryKind','actionKind')
512
+ AND status IN ('active','weak')
513
+ ORDER BY target_type,target_id
514
+ `).all(projectId);
515
+ const buckets = new Map();
516
+ for (const row of rows) {
517
+ const id = nodeId(row.target_type, row.target_id, projectId);
518
+ const bucket = buckets.get(id) ?? { count: 0, evidence: new Set() };
519
+ bucket.count += 1;
520
+ for (const eventId of parseStringArray(row.evidence_event_ids_json).slice(0, 5))
521
+ bucket.evidence.add(eventId);
522
+ buckets.set(id, bucket);
523
+ }
524
+ const update = this.db.prepare(`UPDATE memory_atlas_documents SET support_count=?, evidence_event_ids_json=?, updated_at=? WHERE project_id=? AND node_id=?`);
525
+ for (const [nodeIdValue, bucket] of buckets) {
526
+ update.run(bucket.count, JSON.stringify(Array.from(bucket.evidence).slice(0, 100)), now, projectId, nodeIdValue);
527
+ this.refreshFtsNode(nodeIdValue);
528
+ }
392
529
  }
393
530
  markProjectionFailed(projectId, error, now = Date.now()) {
394
531
  this.db.prepare(`
395
532
  INSERT INTO memory_atlas_projection_state(project_id,projection_name,status,last_rebuild_at,last_error,metadata_json)
396
- VALUES(?,'memory_atlas.v1','error',?,?,'{}')
533
+ VALUES(?,?,'error',?,?,?)
397
534
  ON CONFLICT(project_id,projection_name) DO UPDATE SET status='error',last_rebuild_at=excluded.last_rebuild_at,last_error=excluded.last_error
398
- `).run(projectId, now, error.slice(0, 2000));
535
+ `).run(projectId, MEMORY_ATLAS_PROJECTION_NAME, now, error.slice(0, 2000), JSON.stringify({
536
+ projectionName: MEMORY_ATLAS_PROJECTION_NAME,
537
+ projectionSchemaVersion: MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION,
538
+ }));
399
539
  }
400
540
  getProjectionState(projectId) {
401
- const row = this.db.prepare(`SELECT status,last_rebuild_at,last_error FROM memory_atlas_projection_state WHERE project_id=? AND projection_name='memory_atlas.v1'`).get(projectId);
541
+ const row = this.db.prepare(`SELECT status,last_rebuild_at,last_error FROM memory_atlas_projection_state WHERE project_id=? AND projection_name=?`).get(projectId, MEMORY_ATLAS_PROJECTION_NAME);
402
542
  return row ? { status: row.status, lastRebuildAt: row.last_rebuild_at ?? undefined, lastError: row.last_error || undefined } : null;
403
543
  }
404
544
  listKnownProjectIds() {
@@ -436,6 +576,67 @@ export class MemoryAtlasStore {
436
576
  target: nodeId('topic', row.target_path, projectId), confidence: Number(row.confidence),
437
577
  evidenceEventIds: parseStringArray(row.evidence_event_ids_json) }));
438
578
  }
579
+ episodeEdgesForFacet(projectId, facet) {
580
+ const relationFilter = facet.relation ? 'AND relation_type=?' : '';
581
+ const params = facet.relation
582
+ ? [projectId, facet.type, facet.value, facet.relation]
583
+ : [projectId, facet.type, facet.value];
584
+ return this.db.prepare(`
585
+ SELECT source_id, relation_type, target_type, target_id, confidence, evidence_event_ids_json
586
+ FROM memory_edges
587
+ WHERE project_id=? AND source_type='episode' AND target_type=? AND target_id=? ${relationFilter}
588
+ AND status IN ('active','weak')
589
+ ORDER BY confidence DESC
590
+ LIMIT 5000
591
+ `).all(...params);
592
+ }
593
+ episodeRows(projectId, episodeIds) {
594
+ const ids = Array.from(new Set(episodeIds));
595
+ const rows = [];
596
+ for (let index = 0; index < ids.length; index += 400) {
597
+ const chunk = ids.slice(index, index + 400);
598
+ if (!chunk.length)
599
+ continue;
600
+ rows.push(...this.db.prepare(`
601
+ SELECT d.*, COALESCE(a.activation, 0) AS activation
602
+ FROM memory_atlas_documents d LEFT JOIN memory_atlas_activation a
603
+ ON a.project_id=d.project_id AND a.node_id=d.node_id
604
+ WHERE d.project_id=? AND d.node_id IN (${chunk.map(() => '?').join(',')})
605
+ AND d.node_type='episode' AND d.status NOT IN ('rejected','archived')
606
+ ORDER BY d.occurred_at DESC, d.support_count DESC, d.node_id ASC
607
+ `).all(projectId, ...chunk.map((id) => `episode:${id}`)));
608
+ }
609
+ return rows;
610
+ }
611
+ cardFromEpisodeRow(row, edges, projectId) {
612
+ const metadata = parseMetadata(row.metadata_json);
613
+ const evidenceEventIds = parseStringArray(row.evidence_event_ids_json);
614
+ const matchedFacets = edges.map((edge) => facetFromEdge(edge, projectId));
615
+ const matchedPaths = edges.map((edge) => {
616
+ const facet = facetFromEdge(edge, projectId);
617
+ return { facet, via: [facet.nodeId, row.node_id], relation: edge.relation_type, confidence: Number(edge.confidence || 0) };
618
+ });
619
+ const issueHints = stringArray(metadata.issueHints);
620
+ const topicHints = stringArray(metadata.topicHints);
621
+ const matchedTopicPaths = matchedFacets.filter((facet) => facet.type === 'topic').map((facet) => facet.value);
622
+ return {
623
+ canonicalId: row.node_id,
624
+ nodeType: 'episode',
625
+ displayTitle: row.label,
626
+ oneLineSummary: row.summary || undefined,
627
+ matchedFacets,
628
+ matchedPaths,
629
+ parentTopics: matchedTopicPaths.length ? matchedTopicPaths : topicHints.length ? topicHints : row.topic_path ? [row.topic_path] : [],
630
+ issueType: issueHints[0],
631
+ eventKind: optionalMetadataString(metadata.eventKind),
632
+ localDate: optionalMetadataString(metadata.localDate) ?? (row.occurred_at ? new Date(row.occurred_at).toISOString().slice(0, 10) : undefined),
633
+ whyMatched: matchedFacets.length ? `matched ${matchedFacets.map((facet) => `${facet.type}:${facet.value}`).join(', ')}` : 'matched canonical episode',
634
+ relatedButNotSelected: [],
635
+ evidenceEventIds,
636
+ evidenceTotal: evidenceEventIds.length,
637
+ evidenceReturned: 0,
638
+ };
639
+ }
439
640
  }
440
641
  function mapNode(row, score) {
441
642
  return { id: row.node_id, projectId: row.project_id, nodeType: row.node_type, sourceId: row.source_id,
@@ -479,22 +680,159 @@ catch {
479
680
  } }
480
681
  function escapeLike(value) { return value.replace(/[\\%_]/g, '\\$&'); }
481
682
  function nodeId(type, id, projectId) {
482
- if (type === 'topic' || type === 'time')
683
+ if (type === 'entity')
684
+ return id.startsWith('facet:') ? `entity:${projectId}:${id}` : `entity:${id}`;
685
+ if (['topic', 'time', 'issue', 'session', 'thread', 'memoryKind', 'actionKind'].includes(type))
483
686
  return `${type}:${projectId}:${id}`;
484
687
  return `${type}:${id}`;
485
688
  }
486
689
  function timeNodeId(projectId, occurredAt) {
487
690
  return `time:${projectId}:${new Date(occurredAt).getUTCFullYear()}`;
488
691
  }
692
+ function scopedEntityNodeId(db, value, projectId) {
693
+ if (!value.startsWith('entity:') || value.startsWith(`entity:${projectId}:`))
694
+ return value;
695
+ const id = value.slice('entity:'.length);
696
+ if (!id.startsWith('facet:'))
697
+ return value;
698
+ const scoped = `entity:${projectId}:${id}`;
699
+ const row = db.prepare(`SELECT 1 FROM memory_atlas_documents WHERE project_id=? AND node_id=?`).get(projectId, scoped);
700
+ return row ? scoped : value;
701
+ }
489
702
  function parseNodeId(value, projectId) {
490
- const topicPrefix = `topic:${projectId}:`;
491
- if (value.startsWith(topicPrefix))
492
- return { type: 'topic', id: value.slice(topicPrefix.length) };
493
- const timePrefix = `time:${projectId}:`;
494
- if (value.startsWith(timePrefix))
495
- return { type: 'time', id: value.slice(timePrefix.length) };
703
+ for (const type of ['topic', 'time', 'issue', 'entity', 'session', 'thread', 'memoryKind', 'actionKind']) {
704
+ const prefix = `${type}:${projectId}:`;
705
+ if (value.startsWith(prefix))
706
+ return { type, id: value.slice(prefix.length) };
707
+ }
496
708
  const separator = value.indexOf(':');
497
709
  if (separator <= 0 || separator === value.length - 1)
498
710
  return null;
499
711
  return { type: value.slice(0, separator), id: value.slice(separator + 1) };
500
712
  }
713
+ function parseMetadata(value) {
714
+ try {
715
+ const parsed = JSON.parse(value || '{}');
716
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
717
+ }
718
+ catch {
719
+ return {};
720
+ }
721
+ }
722
+ function stringArray(value) {
723
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.length > 0) : [];
724
+ }
725
+ function optionalMetadataString(value) {
726
+ return typeof value === 'string' && value ? value : undefined;
727
+ }
728
+ function facetFromEdge(edge, projectId) {
729
+ return {
730
+ type: edge.target_type,
731
+ value: edge.target_id,
732
+ label: facetLabel(edge.target_type, edge.target_id),
733
+ nodeId: nodeId(edge.target_type, edge.target_id, projectId),
734
+ relation: edge.relation_type,
735
+ };
736
+ }
737
+ function facetLabel(type, value) {
738
+ if (type === 'topic')
739
+ return value.split('/').pop() || value;
740
+ if (type === 'issue') {
741
+ if (value === 'memory-context-blackbox')
742
+ return 'Memory Context 黑盒';
743
+ if (value === 'graph-runtime-blackbox')
744
+ return 'Graph Runtime 黑盒';
745
+ if (value === 'atlas-readability')
746
+ return 'Atlas 可读性黑盒';
747
+ if (value === 'auto-injection-mismatch')
748
+ return '自动注入不一致';
749
+ }
750
+ return value;
751
+ }
752
+ function groupFacetMatches(matches) {
753
+ const groups = new Map();
754
+ for (const match of matches) {
755
+ const targetSlug = equivalentTargetSlug(match.facet);
756
+ const key = targetSlug
757
+ ? `target:${targetSlug}`
758
+ : match.facet.type === 'actionKind' || match.facet.type === 'memoryKind'
759
+ ? `${match.facet.type}:${match.facet.relation || ''}`
760
+ : `${match.facet.type}:${match.facet.value}:${match.facet.relation || ''}`;
761
+ const group = groups.get(key) ?? [];
762
+ group.push(match);
763
+ groups.set(key, group);
764
+ }
765
+ return Array.from(groups.values());
766
+ }
767
+ function equivalentTargetSlug(facet) {
768
+ if (facet.type === 'entity' && facet.value.startsWith('facet:'))
769
+ return facet.value.slice('facet:'.length);
770
+ if (facet.type !== 'topic')
771
+ return undefined;
772
+ const match = facet.value.match(/^PROJECT\/[^/]+\/([^/]+)$/u);
773
+ return match?.[1];
774
+ }
775
+ function intersectSets(sets) {
776
+ if (!sets.length)
777
+ return new Set();
778
+ const [first, ...rest] = sets;
779
+ const result = new Set();
780
+ for (const value of first ?? []) {
781
+ if (rest.every((set) => set.has(value)))
782
+ result.add(value);
783
+ }
784
+ return result;
785
+ }
786
+ function cardRank(card) {
787
+ return scoreCard(card) + Math.min(card.evidenceTotal || 0, 10) * 0.01;
788
+ }
789
+ function scoreCard(card) {
790
+ return card.matchedFacets.length * 10 + card.matchedPaths.reduce((sum, path) => sum + path.confidence, 0) + (card.localDate ? 0.1 : 0);
791
+ }
792
+ function dedupeCardsByEvidence(cards) {
793
+ const bySignature = new Map();
794
+ const out = [];
795
+ for (const card of cards) {
796
+ const signature = evidenceSignature(card);
797
+ if (!signature) {
798
+ out.push(card);
799
+ continue;
800
+ }
801
+ const kept = bySignature.get(signature);
802
+ if (!kept) {
803
+ bySignature.set(signature, card);
804
+ out.push(card);
805
+ continue;
806
+ }
807
+ kept.relatedButNotSelected = mergeRelatedCards(kept.relatedButNotSelected ?? [], [{
808
+ canonicalId: card.canonicalId,
809
+ displayTitle: card.displayTitle,
810
+ reason: 'same primary raw evidence',
811
+ }]);
812
+ }
813
+ return out;
814
+ }
815
+ function evidenceSignature(card) {
816
+ const primary = card.sourceLocator?.eventId || card.evidenceEventIds[0];
817
+ if (!primary)
818
+ return undefined;
819
+ const facets = (type) => card.matchedFacets
820
+ .filter((facet) => facet.type === type)
821
+ .map((facet) => facet.value)
822
+ .sort()
823
+ .join(',');
824
+ return [
825
+ primary,
826
+ card.localDate || '',
827
+ facets('entity'),
828
+ facets('actionKind'),
829
+ ].join('|');
830
+ }
831
+ function mergeRelatedCards(...groups) {
832
+ const merged = new Map();
833
+ for (const group of groups)
834
+ for (const card of group)
835
+ if (!merged.has(card.canonicalId))
836
+ merged.set(card.canonicalId, card);
837
+ return Array.from(merged.values());
838
+ }
@@ -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
+ }
@@ -122,7 +122,7 @@ cogmem normalize-transcript --input ./hermes-sessions.jsonl --output ./hermes.no
122
122
  cogmem import-hermes --workspace . --project hermes --session ./hermes.normalized.md
123
123
  ```
124
124
 
125
- Cogmem 3.6.4 and later skip import batch sealing for empty episode boundaries and skip legacy empty Dream jobs so one bad imported episode cannot block the queue.
125
+ Cogmem skips import batch sealing for empty episode boundaries and skips legacy empty Dream jobs so one bad imported episode cannot block the queue.
126
126
 
127
127
  After import, use this order:
128
128
 
@@ -162,7 +162,7 @@ cogmem memory list --project hermes --since <globalSeq> --order asc --json
162
162
 
163
163
  `vectors: 0` does not mean Cogmem has no memory. It means the dense vector index has no hot vectors yet. `memory recall` still falls back to governed raw ledger search and returns `sourceContext` locators. Broad inventory questions are expanded into structured cues such as `库存管理`, `在库`, `产品コード`, and `数量`; if compiled-memory candidates miss those cues, raw ledger evidence is preferred.
164
164
 
165
- `sourceContext` and `memory show --json` now share the same replay contract: each event has a `label`, optional `charRange` / `sourceRange`, and `sourceContext.window` / `window` metadata with requested counts, actual counts, `excludesAnchor`, `ordering`, `roleFilter`, and `overlapHandling`. Use those fields before quoting exact wording or explaining what happened before/after a recalled point. In 3.6.5, Raw Ledger list rows and Atlas search/explore evidence include `sourceLocator.command` and `sourceLocator.contextCommand`; run those commands before saying the source event is unavailable.
165
+ `sourceContext` and `memory show --json` share the same replay contract: each event has a `label`, optional `charRange` / `sourceRange`, and `sourceContext.window` / `window` metadata with requested counts, actual counts, `excludesAnchor`, `ordering`, `roleFilter`, and `overlapHandling`. Use those fields before quoting exact wording or explaining what happened before/after a recalled point. Raw Ledger list rows and Atlas search/explore evidence include `sourceLocator.command` and `sourceLocator.contextCommand`; run those commands before saying the source event is unavailable.
166
166
 
167
167
  Check status with:
168
168
 
@@ -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`; in 3.6.5 it scans the historical ledger by cursor and can resume with `--since <globalSeq>`. 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.1 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.6.5 exposes seven 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, time, topic, entity/target, memory-kind, action, and keyword conditions like table filters, so cold memory can be revived without requiring an entity-time-action tuple. Touch only nodes actually used, and follow returned event IDs or `sourceLocator` commands to raw evidence before quoting exact wording.
112
+ Cogmem 3.7.1 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