cogmem 3.6.4 → 3.7.0

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 (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/MEMORY_ATLAS.md +47 -10
  3. package/README.md +36 -13
  4. package/RELEASE_CHECKLIST.md +7 -4
  5. package/dist/agent/AgentMemoryBackend.d.ts +19 -2
  6. package/dist/agent/AgentMemoryBackend.js +134 -3
  7. package/dist/agent/AgentRecallQueryCompiler.d.ts +1 -1
  8. package/dist/agent/AgentRecallQueryCompiler.js +12 -0
  9. package/dist/atlas/EpisodeTitleGenerator.d.ts +31 -0
  10. package/dist/atlas/EpisodeTitleGenerator.js +152 -0
  11. package/dist/atlas/FacetQueryPlanner.d.ts +31 -0
  12. package/dist/atlas/FacetQueryPlanner.js +188 -0
  13. package/dist/atlas/GraphCurator.d.ts +27 -0
  14. package/dist/atlas/GraphCurator.js +397 -0
  15. package/dist/atlas/MemoryAtlasIndexer.d.ts +3 -0
  16. package/dist/atlas/MemoryAtlasIndexer.js +19 -6
  17. package/dist/atlas/MemoryAtlasService.d.ts +5 -1
  18. package/dist/atlas/MemoryAtlasService.js +165 -12
  19. package/dist/atlas/MemoryAtlasTypes.d.ts +89 -11
  20. package/dist/bin/connect.js +85 -13
  21. package/dist/bin/memory.js +221 -18
  22. package/dist/factory.d.ts +2 -0
  23. package/dist/factory.js +36 -18
  24. package/dist/host/openclaw/AutoMemoryPluginInstaller.js +83 -10
  25. package/dist/mcp/server.js +1 -1
  26. package/dist/store/EventStore.d.ts +6 -0
  27. package/dist/store/EventStore.js +28 -2
  28. package/dist/store/MemoryAtlasStore.d.ts +10 -1
  29. package/dist/store/MemoryAtlasStore.js +228 -14
  30. package/dist/types/index.d.ts +3 -0
  31. package/examples/hermes-backend/AGENTS.md +11 -5
  32. package/examples/hermes-backend/README.md +5 -3
  33. package/examples/hermes-backend/SKILL.md +28 -5
  34. package/examples/hermes-backend/references/operations.md +16 -6
  35. package/examples/openclaw-backend/AGENTS.md +9 -3
  36. package/examples/openclaw-backend/README.md +9 -3
  37. package/examples/openclaw-backend/SKILL.md +23 -10
  38. package/examples/openclaw-backend/references/operations.md +21 -8
  39. package/package.json +1 -1
@@ -55,6 +55,8 @@ function readArgs(argv) {
55
55
  before: numberArg(values, 'before'),
56
56
  after: numberArg(values, 'after'),
57
57
  sinceGlobalSeq: numberArg(values, 'since') ?? numberArg(values, 'since-global-seq'),
58
+ untilGlobalSeq: numberArg(values, 'until') ?? numberArg(values, 'until-global-seq'),
59
+ order: orderArg(values, 'order'),
58
60
  intervalMs: numberArg(values, 'interval-ms'),
59
61
  maxRuns: numberArg(values, 'max-runs'),
60
62
  promoteLimit: numberArg(values, 'promote-limit'),
@@ -75,6 +77,7 @@ function usage() {
75
77
  '',
76
78
  'Commands:',
77
79
  ' status summarize raw ledger, vector, and dream backlog state',
80
+ ' plan summarize status as agent-safe next actions',
78
81
  ' list list raw ledger events with source anchors',
79
82
  ' search --query <q> search raw ledger text without requiring hot vectors',
80
83
  ' recall --query <q> run agent-facing governed recall with source context',
@@ -101,7 +104,9 @@ function usage() {
101
104
  ' --thread <id> scope to one thread',
102
105
  ' --session <id> scope to one session',
103
106
  ' --limit <n> result limit, default 20',
104
- ' --since <globalSeq> for bind, scan raw events at or after a global sequence',
107
+ ' --since <globalSeq> list/bind events at or after a global sequence',
108
+ ' --until <globalSeq> list events at or before a global sequence',
109
+ ' --order <asc|desc> list raw ledger order, default desc',
105
110
  ' --status <status> candidate queue status, default candidate',
106
111
  ' --id <candidate> candidate id for review',
107
112
  ' --action <action> review action: approve, reject, defer, supersede, or relink',
@@ -117,7 +122,7 @@ function usage() {
117
122
  ' --interval-ms <n> watch sleep interval, default 300000',
118
123
  ' --max-runs <n> stop watch after n iterations; omit for long-running worker',
119
124
  ' --agent <id> agent id for governed recall, default openclaw',
120
- ' --intent <intent> memory_recall, previous_session_summary, or forensic_quote',
125
+ ' --intent <intent> memory_recall, previous_session_summary, forensic_quote, or historical_discussion',
121
126
  ' --db <memory.db> open an explicit database path',
122
127
  ' --config <toml> open a cogmem TOML config',
123
128
  ' --include-evidence include bounded raw excerpts; event ids are always returned',
@@ -135,6 +140,7 @@ function usage() {
135
140
  }
136
141
  function isMemoryCommand(value) {
137
142
  return value === 'status'
143
+ || value === 'plan'
138
144
  || value === 'list'
139
145
  || value === 'search'
140
146
  || value === 'recall'
@@ -166,9 +172,17 @@ function recallIntentArg(values, key) {
166
172
  const raw = stringArg(values, key);
167
173
  if (!raw)
168
174
  return undefined;
169
- if (raw === 'memory_recall' || raw === 'previous_session_summary' || raw === 'forensic_quote')
175
+ if (raw === 'memory_recall' || raw === 'previous_session_summary' || raw === 'forensic_quote' || raw === 'historical_discussion')
170
176
  return raw;
171
- throw new Error(`--${key} must be one of memory_recall, previous_session_summary, forensic_quote`);
177
+ throw new Error(`--${key} must be one of memory_recall, previous_session_summary, forensic_quote, historical_discussion`);
178
+ }
179
+ function orderArg(values, key) {
180
+ const raw = stringArg(values, key);
181
+ if (!raw)
182
+ return undefined;
183
+ if (raw === 'asc' || raw === 'desc')
184
+ return raw;
185
+ throw new Error(`--${key} must be one of asc, desc`);
172
186
  }
173
187
  function stringArg(values, key) {
174
188
  const value = values[key];
@@ -219,30 +233,32 @@ function inspectionDbPath(args) {
219
233
  function runReadOnlyInspection(args) {
220
234
  const inspection = new MemoryInspectionStore(inspectionDbPath(args));
221
235
  try {
222
- if (args.command === 'status') {
236
+ if (args.command === 'status' || args.command === 'plan') {
223
237
  const payload = inspection.status({
224
238
  projectId: args.projectId, workspaceId: args.workspaceId,
225
239
  threadId: args.threadId, sessionId: args.sessionId,
226
240
  });
241
+ const queueSummary = buildCandidateQueueSummary(inspection, args.projectId, args.limit || 5000);
242
+ const plan = buildMemoryPlan(payload, queueSummary, args.projectId);
243
+ const enrichedPayload = args.command === 'plan'
244
+ ? plan
245
+ : { ...payload, queueSummary, nextActions: plan.nextActions, blocking: plan.blocking, nonBlocking: plan.nonBlocking };
227
246
  if (args.json) {
228
- printCliJson('memory.status', payload, {
247
+ printCliJson(`memory.${args.command}`, enrichedPayload, {
229
248
  queue: payload.dreamCandidateQueue,
230
249
  beliefs: payload.activeBeliefs,
231
250
  });
232
251
  }
233
252
  else
234
- printHuman('status', payload);
253
+ printHuman(args.command, enrichedPayload);
235
254
  return;
236
255
  }
237
- const status = args.status || 'candidate';
238
- const candidates = inspection.listCandidates({
239
- projectId: args.projectId,
240
- status,
241
- limit: args.limit || 50,
242
- });
243
- const payload = { total: candidates.length, status, candidates: candidates.map(candidateToJson) };
256
+ const queueSummary = buildCandidateQueueSummary(inspection, args.projectId, args.limit || 50);
257
+ const payload = args.status
258
+ ? buildSpecificCandidatePayload(inspection, args.projectId, args.status, args.limit || 50, queueSummary)
259
+ : buildGroupedCandidatePayload(queueSummary);
244
260
  if (args.json)
245
- printCliJson('memory.candidates', payload);
261
+ printCliJson('memory.candidates', payload, { queue: queueSummary });
246
262
  else
247
263
  printHuman('candidates', payload);
248
264
  }
@@ -262,6 +278,7 @@ function eventText(event) {
262
278
  }
263
279
  function eventToJson(event) {
264
280
  const text = eventText(event);
281
+ const locator = eventSourceLocator(event, 2, 2);
265
282
  return {
266
283
  eventId: event.eventId,
267
284
  label: memoryEventLabel(event),
@@ -294,6 +311,21 @@ function eventToJson(event) {
294
311
  causalityType: event.causalityType,
295
312
  orderingConfidence: event.orderingConfidence,
296
313
  },
314
+ sourceLocator: locator,
315
+ };
316
+ }
317
+ function eventSourceLocator(event, before, after) {
318
+ const projectArg = event.projectId ? ` --project ${cliArg(event.projectId)}` : '';
319
+ const base = `cogmem memory show --event ${cliArg(event.eventId)}${projectArg}`;
320
+ return {
321
+ eventId: event.eventId,
322
+ globalSeq: event.globalSeq,
323
+ projectId: event.projectId,
324
+ threadId: event.threadId,
325
+ sessionId: event.sessionId,
326
+ localDate: event.localDate,
327
+ command: `${base} --before ${before} --after ${after} --json`,
328
+ contextCommand: `${base} --before 3 --after 3 --json`,
297
329
  };
298
330
  }
299
331
  function candidateToJson(candidate) {
@@ -313,6 +345,141 @@ function candidateToJson(candidate) {
313
345
  updatedAt: candidate.updatedAt,
314
346
  };
315
347
  }
348
+ function buildCandidateQueueSummary(inspection, projectId, limit) {
349
+ const status = inspection.status({ projectId });
350
+ const queue = status.dreamCandidateQueue;
351
+ const candidateRows = inspection.listCandidates({ projectId, status: 'candidate', limit });
352
+ const needsRows = inspection.listCandidates({ projectId, status: 'needs_confirmation', limit: Math.max(limit, 5000) });
353
+ const now = Date.now();
354
+ const deferredRows = needsRows.filter((candidate) => candidate.reviewAfter !== undefined && candidate.reviewAfter > now);
355
+ const activeNeedsRows = needsRows.filter((candidate) => !(candidate.reviewAfter !== undefined && candidate.reviewAfter > now));
356
+ return {
357
+ candidate: queue.candidate,
358
+ needs_confirmation: queue.needsConfirmation,
359
+ activeNeedsConfirmation: Math.max(0, queue.needsConfirmation - deferredRows.length),
360
+ deferredNeedsConfirmation: deferredRows.length,
361
+ promoted: queue.promoted,
362
+ rejected: queue.rejected,
363
+ superseded: queue.superseded,
364
+ shadow: queue.shadow,
365
+ groups: {
366
+ candidate: candidateRows.slice(0, limit).map(candidateToJson),
367
+ needs_confirmation: activeNeedsRows.slice(0, limit).map(candidateToJson),
368
+ deferred: deferredRows.slice(0, limit).map(candidateToJson),
369
+ },
370
+ };
371
+ }
372
+ function buildSpecificCandidatePayload(inspection, projectId, status, limit, queueSummary) {
373
+ const candidates = inspection.listCandidates({ projectId, status, limit });
374
+ const payload = {
375
+ total: candidates.length,
376
+ status,
377
+ candidates: candidates.map(candidateToJson),
378
+ queueSummary,
379
+ };
380
+ if (status === 'candidate') {
381
+ payload.warning = 'This lists only status=candidate. Use memory candidates without --status for grouped candidate and needs_confirmation queues.';
382
+ }
383
+ else if (status === 'needs_confirmation') {
384
+ payload.warning = 'memory govern does not process needs_confirmation. Use memory review with explicit user evidence, or defer with review_after.';
385
+ }
386
+ return payload;
387
+ }
388
+ function buildGroupedCandidatePayload(queueSummary) {
389
+ return {
390
+ total: queueSummary.groups.candidate.length + queueSummary.groups.needs_confirmation.length + queueSummary.groups.deferred.length,
391
+ status: 'grouped',
392
+ queueSummary,
393
+ groups: queueSummary.groups,
394
+ nextActions: candidateNextActions(queueSummary, undefined),
395
+ };
396
+ }
397
+ function buildMemoryPlan(status, queueSummary, projectId) {
398
+ const nextActions = [];
399
+ const blocking = [];
400
+ const nonBlocking = [];
401
+ const projectArg = projectId ? ` --project ${cliArg(projectId)}` : '';
402
+ const episodeDream = status.episodeDream;
403
+ const pendingEpisodes = asCount(episodeDream.pending) + asCount(episodeDream.retryScheduled);
404
+ const undreamedRawCount = asCount(status.undreamedRawCount);
405
+ if (pendingEpisodes > 0) {
406
+ nextActions.push({
407
+ priority: 'high',
408
+ type: 'dream_tick',
409
+ safeForAutomation: true,
410
+ reason: `${pendingEpisodes} sealed episode Dream jobs can be processed`,
411
+ command: `cogmem dream tick${projectArg} --mode auto --max-episodes 20 --json`,
412
+ });
413
+ }
414
+ if (undreamedRawCount > 0) {
415
+ nonBlocking.push({
416
+ type: 'raw_dream_ledger_lag',
417
+ count: undreamedRawCount,
418
+ episodeDreamPending: pendingEpisodes,
419
+ resolvableByDreamTick: false,
420
+ safeForAutomation: false,
421
+ reason: pendingEpisodes > 0
422
+ ? 'Raw ledger dream coverage is tracked separately from sealed episode Dream jobs; dream tick may process episodes but does not guarantee this counter will clear.'
423
+ : 'Raw ledger dream coverage is behind, but there are no sealed episode Dream jobs. Do not run dream tick for this signal alone.',
424
+ command: `cogmem memory list${projectArg} --order asc --limit 20 --json`,
425
+ });
426
+ }
427
+ nextActions.push(...candidateNextActions(queueSummary, projectId));
428
+ if (queueSummary.activeNeedsConfirmation > 0) {
429
+ blocking.push({
430
+ type: 'needs_confirmation',
431
+ count: queueSummary.activeNeedsConfirmation,
432
+ reason: 'Manual review requires explicit user evidence; memory govern will not process these candidates.',
433
+ command: `cogmem memory candidates${projectArg} --status needs_confirmation --json`,
434
+ });
435
+ }
436
+ if (queueSummary.deferredNeedsConfirmation > 0) {
437
+ nonBlocking.push({
438
+ type: 'deferred_confirmation',
439
+ count: queueSummary.deferredNeedsConfirmation,
440
+ reason: 'Deferred needs_confirmation remains in the review queue until review_after.',
441
+ command: `cogmem memory candidates${projectArg} --json`,
442
+ });
443
+ }
444
+ return {
445
+ healthy: nextActions.length === 0 && blocking.length === 0,
446
+ projectId,
447
+ queueSummary,
448
+ dreamBacklog: status.dreamBacklog,
449
+ episodeDream: status.episodeDream,
450
+ vectorState: status.vectorState,
451
+ nextActions,
452
+ blocking,
453
+ nonBlocking,
454
+ };
455
+ }
456
+ function candidateNextActions(queueSummary, projectId) {
457
+ const projectArg = projectId ? ` --project ${cliArg(projectId)}` : '';
458
+ const actions = [];
459
+ if (queueSummary.candidate > 0) {
460
+ actions.push({
461
+ priority: 'high',
462
+ type: 'govern',
463
+ safeForAutomation: true,
464
+ reason: `${queueSummary.candidate} ordinary candidates are ready for deterministic governance`,
465
+ command: `cogmem memory govern${projectArg} --limit 100 --json`,
466
+ });
467
+ }
468
+ if (queueSummary.activeNeedsConfirmation > 0) {
469
+ actions.push({
470
+ priority: 'medium',
471
+ type: 'review_needs_confirmation',
472
+ safeForAutomation: false,
473
+ reason: `${queueSummary.activeNeedsConfirmation} candidates need explicit operator/user confirmation`,
474
+ command: `cogmem memory candidates${projectArg} --status needs_confirmation --json`,
475
+ reviewCommand: `cogmem memory review${projectArg} --id <candidate-id> --action <approve|reject|defer|supersede|relink> --actor <operator> --reason <reason> --json`,
476
+ });
477
+ }
478
+ return actions;
479
+ }
480
+ function asCount(value) {
481
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
482
+ }
316
483
  function runStatus(kernel, args) {
317
484
  const page = kernel.eventStore.queryEvents(1, 1, {
318
485
  projectId: args.projectId ? [args.projectId] : undefined,
@@ -344,9 +511,15 @@ function runList(kernel, args) {
344
511
  workspaceId: args.workspaceId ? [args.workspaceId] : undefined,
345
512
  threadId: args.threadId ? [args.threadId] : undefined,
346
513
  sessionId: args.sessionId ? [args.sessionId] : undefined,
514
+ sinceGlobalSeq: args.sinceGlobalSeq,
515
+ untilGlobalSeq: args.untilGlobalSeq,
516
+ order: args.order,
347
517
  });
348
518
  return {
349
519
  total: page.total,
520
+ sinceGlobalSeq: args.sinceGlobalSeq,
521
+ untilGlobalSeq: args.untilGlobalSeq,
522
+ order: args.order || 'desc',
350
523
  events: page.records.map(eventToJson),
351
524
  };
352
525
  }
@@ -400,6 +573,9 @@ function runRecall(kernel, args) {
400
573
  decisionTrace: result.decisionTrace,
401
574
  strategyCapsule,
402
575
  narrative: result.narrative,
576
+ atlasCards: result.atlasCards,
577
+ relatedButNotSelected: result.relatedButNotSelected,
578
+ relaxationTrace: result.relaxationTrace,
403
579
  items: result.items,
404
580
  };
405
581
  }
@@ -607,12 +783,24 @@ function runCandidates(kernel, args) {
607
783
  };
608
784
  }
609
785
  function printHuman(command, payload) {
610
- if (command === 'status') {
786
+ if (command === 'status' || command === 'plan') {
787
+ if (command === 'plan') {
788
+ console.log(`healthy: ${payload.healthy}`);
789
+ console.log(`queueSummary: ${JSON.stringify(payload.queueSummary)}`);
790
+ console.log(`nextActions: ${JSON.stringify(payload.nextActions)}`);
791
+ console.log(`blocking: ${JSON.stringify(payload.blocking)}`);
792
+ console.log(`nonBlocking: ${JSON.stringify(payload.nonBlocking)}`);
793
+ return;
794
+ }
611
795
  console.log(`rawEvents: ${payload.rawEventCount}`);
612
796
  console.log(`vectors: ${payload.vectorCount}`);
613
797
  console.log(`dreamBacklog: ${JSON.stringify(payload.dreamBacklog)}`);
614
798
  console.log(`episodeDream: ${JSON.stringify(payload.episodeDream)}`);
615
799
  console.log(`dreamCandidateQueue: ${JSON.stringify(payload.dreamCandidateQueue)}`);
800
+ if (payload.queueSummary)
801
+ console.log(`queueSummary: ${JSON.stringify(payload.queueSummary)}`);
802
+ if (payload.nextActions)
803
+ console.log(`nextActions: ${JSON.stringify(payload.nextActions)}`);
616
804
  return;
617
805
  }
618
806
  if (command === 'dream') {
@@ -646,6 +834,18 @@ function printHuman(command, payload) {
646
834
  return;
647
835
  }
648
836
  if (command === 'candidates') {
837
+ if (payload.queueSummary)
838
+ console.log(`queueSummary: ${JSON.stringify(payload.queueSummary)}`);
839
+ const groups = payload.groups;
840
+ if (groups) {
841
+ for (const [group, values] of Object.entries(groups)) {
842
+ console.log(`${group}: ${values.length}`);
843
+ for (const candidate of values) {
844
+ console.log(`- ${candidate.candidateId} ${candidate.candidateType} ${candidate.status} confidence=${candidate.confidence}`);
845
+ }
846
+ }
847
+ return;
848
+ }
649
849
  const candidates = Array.isArray(payload.candidates) ? payload.candidates : [];
650
850
  for (const candidate of candidates) {
651
851
  console.log(`- ${candidate.candidateId} ${candidate.candidateType} ${candidate.status} confidence=${candidate.confidence}`);
@@ -684,7 +884,7 @@ function printHuman(command, payload) {
684
884
  return;
685
885
  }
686
886
  if (command === 'graph' || command.startsWith('graph-')) {
687
- console.log(`memoryAtlas: ${payload.version || 'memory_atlas.v1'} project=${payload.projectId || 'unknown'}`);
887
+ console.log(`memoryAtlas: ${payload.version || 'memory_atlas.v2'} project=${payload.projectId || 'unknown'}`);
688
888
  const rows = Array.isArray(payload.nodes) ? payload.nodes
689
889
  : Array.isArray(payload.path) ? payload.path
690
890
  : Array.isArray(payload.actions) ? payload.actions
@@ -741,7 +941,7 @@ async function main() {
741
941
  console.log(usage());
742
942
  return;
743
943
  }
744
- if (args.command === 'status' || args.command === 'candidates') {
944
+ if (args.command === 'status' || args.command === 'plan' || args.command === 'candidates') {
745
945
  runReadOnlyInspection(args);
746
946
  return;
747
947
  }
@@ -801,3 +1001,6 @@ main().catch((error) => {
801
1001
  console.error(error instanceof Error ? error.message : String(error));
802
1002
  process.exit(1);
803
1003
  });
1004
+ function cliArg(value) {
1005
+ return /^[A-Za-z0-9._:/=@+-]+$/u.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
1006
+ }
package/dist/factory.d.ts CHANGED
@@ -138,6 +138,8 @@ export interface MemoryBindingBackfillOptions {
138
138
  export interface MemoryBindingBackfillResult {
139
139
  projectId?: string;
140
140
  sinceGlobalSeq?: number;
141
+ nextGlobalSeq?: number;
142
+ hasMore: boolean;
141
143
  scannedEvents: number;
142
144
  bindableEvents: number;
143
145
  boundEvents: number;
package/dist/factory.js CHANGED
@@ -83,7 +83,7 @@ import { SqliteVecStore } from './store/SqliteVecStore.js';
83
83
  import { VectorStore } from './store/VectorStore.js';
84
84
  import { config } from './utils/Config.js';
85
85
  import { KernelRunningError, SnapshotExporter, SnapshotImporter, } from './snapshot/index.js';
86
- const CORE_VERSION = '3.6.4';
86
+ const CORE_VERSION = '3.7.0';
87
87
  const LATEST_SCHEMA_VERSION = 27;
88
88
  export class MemoryKernel {
89
89
  options;
@@ -1195,18 +1195,22 @@ export class MemoryKernel {
1195
1195
  }
1196
1196
  bindRawEvents(options = {}) {
1197
1197
  const limit = Math.max(1, Math.min(options.limit ?? 500, 5000));
1198
- const page = this.eventStore.queryEvents(1, limit, {
1199
- projectId: options.projectId ? [options.projectId] : undefined,
1200
- workspaceId: options.workspaceId ? [options.workspaceId] : undefined,
1201
- threadId: options.threadId ? [options.threadId] : undefined,
1202
- sessionId: options.sessionId ? [options.sessionId] : undefined,
1198
+ const afterGlobalSeq = options.sinceGlobalSeq === undefined
1199
+ ? undefined
1200
+ : Math.max(-1, options.sinceGlobalSeq - 1);
1201
+ const records = this.eventStore.listRawEventsAfterGlobalSeq({
1202
+ projectId: options.projectId,
1203
+ workspaceId: options.workspaceId,
1204
+ threadId: options.threadId,
1205
+ sessionId: options.sessionId,
1206
+ afterGlobalSeq,
1207
+ limit,
1203
1208
  });
1204
- const records = page.records
1205
- .filter((event) => options.sinceGlobalSeq === undefined || (event.globalSeq || 0) >= options.sinceGlobalSeq)
1206
- .sort((a, b) => (a.globalSeq || 0) - (b.globalSeq || 0));
1207
1209
  const result = {
1208
1210
  projectId: options.projectId,
1209
1211
  sinceGlobalSeq: options.sinceGlobalSeq,
1212
+ nextGlobalSeq: records.at(-1)?.globalSeq,
1213
+ hasMore: records.length >= limit,
1210
1214
  scannedEvents: records.length,
1211
1215
  bindableEvents: 0,
1212
1216
  boundEvents: 0,
@@ -1325,16 +1329,30 @@ export class MemoryKernel {
1325
1329
  return { touched: this.memoryAtlasStore.recordAccess(input.projectId, valid, input.reason, input.query, input.now) };
1326
1330
  }
1327
1331
  countUnboundBindableRawEvents(projectId, limit = 1000) {
1328
- const page = this.eventStore.queryEvents(1, Math.max(1, limit), {
1329
- projectId: projectId ? [projectId] : undefined,
1330
- });
1332
+ const max = Math.max(1, limit);
1333
+ let afterGlobalSeq;
1331
1334
  let count = 0;
1332
- for (const event of page.records) {
1333
- if (!this.memoryBindingService.isBindableRawEvent(event))
1334
- continue;
1335
- if (this.memoryBindingStore.listBindings({ eventId: event.eventId, limit: 1 }).length > 0)
1336
- continue;
1337
- count += 1;
1335
+ while (count < max) {
1336
+ const batchLimit = Math.min(500, max);
1337
+ const events = this.eventStore.listRawEventsAfterGlobalSeq({
1338
+ projectId,
1339
+ afterGlobalSeq,
1340
+ limit: batchLimit,
1341
+ });
1342
+ if (events.length === 0)
1343
+ break;
1344
+ afterGlobalSeq = events.at(-1)?.globalSeq ?? afterGlobalSeq;
1345
+ for (const event of events) {
1346
+ if (!this.memoryBindingService.isBindableRawEvent(event))
1347
+ continue;
1348
+ if (this.memoryBindingStore.listBindings({ eventId: event.eventId, limit: 1 }).length > 0)
1349
+ continue;
1350
+ count += 1;
1351
+ if (count >= max)
1352
+ break;
1353
+ }
1354
+ if (events.length < batchLimit)
1355
+ break;
1338
1356
  }
1339
1357
  return count;
1340
1358
  }
@@ -5,7 +5,7 @@ import { basename, dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { resolveCogmemConfigPath } from '../../config/CogmemConfig.js';
7
7
  const PLUGIN_ID = 'cogmem-auto-memory';
8
- const PLUGIN_VERSION = '0.6.3';
8
+ const PLUGIN_VERSION = '0.7.0';
9
9
  function defaultPublicEntrypoint() {
10
10
  return join(resolve(dirname(fileURLToPath(import.meta.url)), '../..'), 'public.js');
11
11
  }
@@ -26,7 +26,7 @@ export function installOpenClawAutoMemoryPlugin(options) {
26
26
  ? { kind: 'toml', path: resolve(options.configPath) }
27
27
  : resolveCogmemConfigPath({ cwd: workspaceRoot });
28
28
  if (configResolution.kind !== 'toml') {
29
- throw new Error(`Missing cogmem config at ${configResolution.path}. Run cogmem-init --agent openclaw --scope project first.`);
29
+ throw new Error(`Missing cogmem config at ${configResolution.path}. Ask an operator to run the interactive setup: cogmem init --agent openclaw --scope project.`);
30
30
  }
31
31
  const configPath = configResolution.path;
32
32
  const pluginDir = resolve(options.pluginDir || defaultOpenClawAutoMemoryPluginDir(workspaceRoot));
@@ -663,6 +663,9 @@ function classifyRecallIntent(query) {
663
663
  if (/原话|怎么说的|完整对话|上一句|下一句|exact quote|verbatim/.test(text)) {
664
664
  return 'forensic_quote';
665
665
  }
666
+ if (/记得.{0,12}(聊过|讨论过|说过)|还记得|之前.{0,12}(聊过|讨论过|说过)|以前.{0,12}(聊过|讨论过|说过)|(过去|几个月前|半年前|上个月|前几天|昨天|上次|上个).{0,20}(聊过|讨论过|说过)|当时.{0,12}(聊|说|问)|那次.{0,12}(聊|说|问)|有没有.{0,12}(记录|聊过|讨论过)|have we discussed|did we talk about|previously discussed/.test(text)) {
667
+ return 'historical_discussion';
668
+ }
666
669
  return 'memory_recall';
667
670
  }
668
671
 
@@ -989,7 +992,7 @@ function audit(config, record) {
989
992
  const plugin = {
990
993
  id: PLUGIN_ID,
991
994
  name: 'CogMem Auto Memory',
992
- version: '0.6.3',
995
+ version: '${PLUGIN_VERSION}',
993
996
  register(api) {
994
997
  if (!api || typeof api.on !== 'function') {
995
998
  throw new Error('OpenClaw plugin API missing api.on');
@@ -1335,10 +1338,32 @@ async function recallPayload(input, config, kernel, memory, formatStrategyContex
1335
1338
  maxMemoryRatio: Number(config.contextMemoryMaxRatio || 0.25), strategy: strategyCapsule,
1336
1339
  candidates: result.items.map(contextCandidateFromRecallItem),
1337
1340
  });
1338
- const plannedResult = activationPlan
1341
+ let plannedResult = activationPlan
1339
1342
  ? { ...result, items: activationPlan.selected.map((candidate) => candidate.recallItem) }
1340
1343
  : result;
1344
+ let activationReceipt = activationPlan && activationPlan.receipt;
1345
+ if (activationPlan && Array.isArray(result.items)) {
1346
+ const selectedIds = new Set(plannedResult.items.map((item) => item && item.id).filter(Boolean));
1347
+ const strictFacetItem = (result.decisionTrace && result.decisionTrace.selectedLane === 'facet_graph_raw_ledger')
1348
+ ? result.items.find((item) => item && item.canonicalId && Array.isArray(item.matchedFacets) && item.matchedFacets.length > 0)
1349
+ : undefined;
1350
+ if (strictFacetItem && !selectedIds.has(strictFacetItem.id)) {
1351
+ plannedResult = { ...plannedResult, items: [strictFacetItem, ...plannedResult.items].slice(0, Number(config.limit || 3)) };
1352
+ activationReceipt = {
1353
+ ...(activationReceipt || {}),
1354
+ facetGraphRetained: true,
1355
+ retainedCanonicalId: strictFacetItem.canonicalId,
1356
+ matchedFacets: strictFacetItem.matchedFacets,
1357
+ reason: 'strict_facet_match_must_not_be_silently_dropped',
1358
+ };
1359
+ }
1360
+ }
1341
1361
  const anchorItem = plannedResult.items.find((item) => item && item.sourceAnchor && item.sourceAnchor.eventId);
1362
+ const selectedCanonicalIds = new Set(plannedResult.items.map((item) => item && item.canonicalId).filter(Boolean));
1363
+ const selectedEpisodeCards = Array.isArray(result.atlasCards)
1364
+ ? result.atlasCards.filter((card) => card && selectedCanonicalIds.has(card.canonicalId))
1365
+ : [];
1366
+ plannedResult = { ...plannedResult, atlasCards: selectedEpisodeCards };
1342
1367
  const recallContext = formatRecallContext(plannedResult, config);
1343
1368
  return {
1344
1369
  context: recallContext ? (strategyCapsule ? formatStrategyContext(strategyCapsule) + '\n\n' : '') + recallContext : '',
@@ -1346,7 +1371,9 @@ async function recallPayload(input, config, kernel, memory, formatStrategyContex
1346
1371
  recallMode: result.recallMode, fallbackUsed: result.fallbackUsed, intent: input.intent || 'memory_recall',
1347
1372
  anchorEventId: anchorItem && anchorItem.sourceAnchor && anchorItem.sourceAnchor.eventId,
1348
1373
  anchorText: anchorItem && anchorItem.text, queryPlan: result.queryPlan, decisionTrace: result.decisionTrace,
1349
- activationReceipt: activationPlan && activationPlan.receipt, strategyCapsule,
1374
+ atlasCards: result.atlasCards, selectedEpisodeCards,
1375
+ relatedButNotSelected: result.relatedButNotSelected, relaxationTrace: result.relaxationTrace,
1376
+ activationReceipt, strategyCapsule,
1350
1377
  strategyReplanned: replan.replanned, strategyReplanReason: replan.reason,
1351
1378
  recallLatencyMs: Date.now() - recallStartedAt,
1352
1379
  };
@@ -1391,16 +1418,27 @@ function stripCogmemRecallBlocks(text) {
1391
1418
 
1392
1419
  function formatAtlasContext(result, maxChars) {
1393
1420
  const nodes = Array.isArray(result && result.nodes) ? result.nodes : [];
1394
- if (!nodes.length) return '';
1421
+ const cards = Array.isArray(result && result.cards) ? result.cards : [];
1422
+ if (!nodes.length && !cards.length) return '';
1395
1423
  const edges = Array.isArray(result && result.edges) ? result.edges : [];
1396
1424
  const actions = Array.isArray(result && result.nextActions) ? result.nextActions : [];
1397
1425
  const nodeDetails = Array.isArray(result && result.nodeDetails) ? result.nodeDetails : [];
1398
1426
  const lines = [
1399
- '<COGMEM_MEMORY_ATLAS version="memory_atlas.v1" volatile="true" persistence="forbidden" evidence_authority="raw_event_ids_only">',
1427
+ '<COGMEM_MEMORY_ATLAS version="memory_atlas.v2" volatile="true" persistence="forbidden" evidence_authority="raw_event_ids_only">',
1400
1428
  'Bounded navigation map; use it to choose nodes and paths, not as durable evidence.',
1401
- 'Matched facets: ' + safeAtlasText(JSON.stringify(result.facets || {}), 1000),
1429
+ 'Matched facets: ' + safeAtlasText(JSON.stringify(result.facets && result.facets.planner || result.facets || {}), 1400),
1402
1430
  'Cold memory resurrected: ' + String(result.coldMemoryResurrected === true),
1403
1431
  '',
1432
+ 'Selected memory cards:',
1433
+ ...cards.slice(0, 8).map((card) => '- ' + formatAtlasCard(card)),
1434
+ ...(cards.length ? [
1435
+ '',
1436
+ 'Related but not selected:',
1437
+ ...cards.flatMap((card) => Array.isArray(card.relatedButNotSelected) ? card.relatedButNotSelected : [])
1438
+ .slice(0, 8)
1439
+ .map((card) => '- ' + safeAtlasText(card.displayTitle, 160) + ' [' + safeAtlasText(card.reason, 160) + ']'),
1440
+ '',
1441
+ ] : []),
1404
1442
  'Nodes:',
1405
1443
  ...nodes.slice(0, 30).map((node) => '- ' + JSON.stringify({
1406
1444
  id: safeAtlasText(node.id, 500), type: safeAtlasText(node.nodeType, 80),
@@ -1692,6 +1730,9 @@ function compactRecallItems(items, config) {
1692
1730
  projectId: item.projectId,
1693
1731
  sourceAnchor: item.sourceAnchor,
1694
1732
  whyMatched: item.whyMatched,
1733
+ canonicalId: item.canonicalId,
1734
+ displayTitle: item.displayTitle,
1735
+ matchedFacets: item.matchedFacets,
1695
1736
  }));
1696
1737
  }
1697
1738
 
@@ -1728,6 +1769,15 @@ function formatRecallContext(result, config) {
1728
1769
  if (result.decisionTrace) {
1729
1770
  lines.push('recallDecision=' + formatRecallDecision(result.decisionTrace));
1730
1771
  }
1772
+ if (Array.isArray(result.atlasCards) && result.atlasCards.length) {
1773
+ lines.push('selectedMemoryCards=' + result.atlasCards.slice(0, Number(config.memoryContextMaxItems || config.limit || 3)).map(formatAtlasCard).join(' | '));
1774
+ }
1775
+ if (Array.isArray(result.relaxationTrace) && result.relaxationTrace.length) {
1776
+ lines.push('relaxationTrace=' + result.relaxationTrace.map((step) => step.from + ' -> ' + step.to + ' (' + step.reason + ')').join(' | '));
1777
+ }
1778
+ if (Array.isArray(result.relatedButNotSelected) && result.relatedButNotSelected.length) {
1779
+ lines.push('relatedButNotSelected=' + result.relatedButNotSelected.slice(0, 4).map((item) => item.displayTitle + ' [' + item.reason + ']').join(' | '));
1780
+ }
1731
1781
  lines.push('');
1732
1782
  if (result.narrative && result.narrative.summary) {
1733
1783
  lines.push(result.narrative.summary);
@@ -1745,7 +1795,11 @@ function formatRecallContext(result, config) {
1745
1795
  + (item.sourceAnchor.sessionId ? '; session=' + item.sourceAnchor.sessionId : '')
1746
1796
  + (item.sourceAnchor.role ? '; role=' + item.sourceAnchor.role : '') : '';
1747
1797
  const why = item.whyMatched ? '; whyMatched=' + item.whyMatched : '';
1748
- lines.push(' sourceType=' + sourceType + '; confidence=' + confidence + '; canAnswerExactQuote=' + quote + anchor + why);
1798
+ const canonical = item.canonicalId ? '; canonicalId=' + item.canonicalId : '';
1799
+ const matchedFacets = Array.isArray(item.matchedFacets) && item.matchedFacets.length
1800
+ ? '; matchedFacets=' + item.matchedFacets.map((facet) => facet.type + ':' + facet.value).join(',')
1801
+ : '';
1802
+ lines.push(' sourceType=' + sourceType + '; confidence=' + confidence + '; canAnswerExactQuote=' + quote + canonical + matchedFacets + anchor + why);
1749
1803
  if (item.sourceContext && item.sourceContext.event) {
1750
1804
  const anchorEvent = item.sourceContext.event;
1751
1805
  const anchorFormatted = formatContextEvent(anchorEvent, Math.min(220, sourceWindowMaxChars));
@@ -1754,7 +1808,8 @@ function formatRecallContext(result, config) {
1754
1808
  if (item.sourceContext.locator && item.sourceContext.locator.command) {
1755
1809
  lines.push(' sourceLocator=' + item.sourceContext.locator.command);
1756
1810
  } else if (item.sourceContext.event.eventId) {
1757
- lines.push(' sourceLocator=cogmem memory show --event ' + item.sourceContext.event.eventId + ' --before 2 --after 2');
1811
+ const project = item.sourceContext.event.projectId ? ' --project ' + item.sourceContext.event.projectId : '';
1812
+ lines.push(' sourceLocator=cogmem memory show --event ' + item.sourceContext.event.eventId + project + ' --before 2 --after 2 --json');
1758
1813
  }
1759
1814
  const seenEventIds = new Set([anchorEvent.eventId].filter(Boolean));
1760
1815
  const before = uniqueWindowEvents(Array.isArray(item.sourceContext.before) ? item.sourceContext.before : [], seenEventIds).slice(-2);
@@ -1797,6 +1852,24 @@ function formatRecallDecision(trace) {
1797
1852
  + ',raw:' + Number(counts.rawLedger || 0);
1798
1853
  }
1799
1854
 
1855
+ function formatAtlasCard(card) {
1856
+ const facets = Array.isArray(card && card.matchedFacets)
1857
+ ? card.matchedFacets.map((facet) => facet.type + ':' + facet.value).join(',')
1858
+ : '';
1859
+ const paths = Array.isArray(card && card.matchedPaths)
1860
+ ? card.matchedPaths.map((path) => path.relation + '@' + (path.facet && path.facet.nodeId || 'facet')).slice(0, 6).join(',')
1861
+ : '';
1862
+ const locator = card && card.sourceLocator && card.sourceLocator.command ? '; sourceLocator=' + card.sourceLocator.command : '';
1863
+ const why = card && card.whyMatched ? '; whyMatched=' + truncateLineWithMeta(card.whyMatched, 180).text : '';
1864
+ return (card && card.canonicalId || 'episode:unknown')
1865
+ + '; title=' + truncateLineWithMeta(card && card.displayTitle, 120).text
1866
+ + '; summary=' + truncateLineWithMeta(card && card.oneLineSummary, 180).text
1867
+ + (facets ? '; matchedFacets=' + facets : '')
1868
+ + (paths ? '; matchedPaths=' + paths : '')
1869
+ + why
1870
+ + locator;
1871
+ }
1872
+
1800
1873
  function clampRecallContext(text, maxChars) {
1801
1874
  const closingTag = '</COGMEM_RECALL_CONTEXT>';
1802
1875
  if (text.length <= maxChars) return text;
@@ -5,7 +5,7 @@ import { callCogmemMcpTool, listCogmemMcpTools, } from './CoreMcpTools.js';
5
5
  export function createCogmemMcpServer(runtime = {}) {
6
6
  const server = new Server({
7
7
  name: 'cogmem-core',
8
- version: '3.6.1',
8
+ version: '3.7.0',
9
9
  }, {
10
10
  capabilities: {
11
11
  tools: {},