cogmem 3.6.3 → 3.6.5

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.
@@ -90,22 +90,92 @@ function defaultSkillPath(agent, workspaceRoot) {
90
90
  return join(process.env.HOME || homedir(), '.hermes', 'skills', 'cogmem-memory', 'SKILL.md');
91
91
  }
92
92
  function nextCommands(agent) {
93
+ return nextSteps(agent)
94
+ .filter((step) => step.safeForAutomation && step.actor === 'agent')
95
+ .map((step) => step.command);
96
+ }
97
+ function nextSteps(agent) {
98
+ const project = agent === 'openclaw' ? 'openclaw' : 'hermes';
99
+ const importCommand = agent === 'openclaw' ? 'import-openclaw' : 'import-hermes';
100
+ const steps = [
101
+ {
102
+ id: 'doctor',
103
+ actor: 'agent',
104
+ safeForAutomation: true,
105
+ command: './node_modules/.bin/cogmem doctor',
106
+ when: 'after installation or update',
107
+ purpose: 'verify package, config, and database access',
108
+ },
109
+ {
110
+ id: 'import_dry_run',
111
+ actor: 'agent',
112
+ safeForAutomation: true,
113
+ command: `./node_modules/.bin/cogmem ${importCommand} --workspace . --project ${project} --dry-run --json`,
114
+ when: 'before importing legacy memory',
115
+ purpose: 'preview source discovery and import counts',
116
+ },
117
+ {
118
+ id: 'import_apply',
119
+ actor: 'agent',
120
+ safeForAutomation: true,
121
+ command: `./node_modules/.bin/cogmem ${importCommand} --workspace . --project ${project} --json`,
122
+ when: 'after dry-run looks correct',
123
+ purpose: 'idempotently import legacy memory into Cogmem',
124
+ },
125
+ {
126
+ id: 'memory_plan',
127
+ actor: 'agent',
128
+ safeForAutomation: true,
129
+ command: `./node_modules/.bin/cogmem memory plan --project ${project} --json`,
130
+ when: 'after import, update, or user asks for governance progress',
131
+ purpose: 'read agent-safe next actions instead of guessing from raw counters',
132
+ },
133
+ {
134
+ id: 'memory_status',
135
+ actor: 'agent',
136
+ safeForAutomation: true,
137
+ command: `./node_modules/.bin/cogmem memory status --project ${project} --json`,
138
+ when: 'for read-only health checks',
139
+ purpose: 'inspect raw ledger, vector fallback, dream backlog, and queue summary',
140
+ },
141
+ {
142
+ id: 'interactive_init',
143
+ actor: 'operator',
144
+ safeForAutomation: false,
145
+ command: `./node_modules/.bin/cogmem init --agent ${agent} --scope project`,
146
+ when: 'only if no Cogmem config exists and a human operator is present',
147
+ purpose: 'interactive first-time config wizard; agents must not run this unattended',
148
+ },
149
+ ];
93
150
  if (agent === 'openclaw') {
94
- return [
95
- './node_modules/.bin/cogmem-init --agent openclaw --scope project',
96
- './node_modules/.bin/cogmem-doctor',
97
- './node_modules/.bin/cogmem-import-openclaw --workspace . --project openclaw --dry-run',
98
- './node_modules/.bin/cogmem-import-openclaw --workspace . --project openclaw',
99
- './node_modules/.bin/cogmem-connect openclaw --workspace . --auto --force',
100
- ];
151
+ steps.splice(1, 0, {
152
+ id: 'openclaw_diagnose',
153
+ actor: 'agent',
154
+ safeForAutomation: true,
155
+ command: './node_modules/.bin/cogmem openclaw diagnose --workspace . --json',
156
+ when: 'after connect --auto --force or before investigating injection failures',
157
+ purpose: 'verify generated plugin files, audit log, queue lock, and hook diagnostics',
158
+ });
159
+ steps.push({
160
+ id: 'restart_gateway',
161
+ actor: 'host',
162
+ safeForAutomation: false,
163
+ command: 'openclaw gateway restart',
164
+ when: 'after plugin files or OpenClaw config changed',
165
+ purpose: 'reload OpenClaw hooks so the refreshed Cogmem plugin is active',
166
+ });
101
167
  }
102
- return [
103
- './node_modules/.bin/cogmem-init --agent hermes',
104
- './node_modules/.bin/cogmem-doctor',
105
- './node_modules/.bin/cogmem-connect hermes --workspace . --auto --force',
106
- './node_modules/.bin/cogmem-import-hermes --workspace . --project hermes --dry-run',
107
- './node_modules/.bin/cogmem-import-hermes --workspace . --project hermes',
108
- ];
168
+ else {
169
+ steps.push({
170
+ id: 'reload_hermes',
171
+ actor: 'host',
172
+ safeForAutomation: false,
173
+ command: 'restart or reload Hermes MCP server',
174
+ when: 'after Hermes MCP config changed',
175
+ purpose: 'reload the cogmem MCP server entry',
176
+ });
177
+ }
178
+ return steps;
109
179
  }
110
180
  function usage() {
111
181
  return [
@@ -138,33 +208,45 @@ function hostConfigSnippet(agent, workspaceRoot, auto) {
138
208
  '// Add host config only after installing a real OpenClaw plugin wrapper with a valid manifest/schema.',
139
209
  ].join('\n');
140
210
  }
141
- const mcpBin = resolveCogmemMcpCommand(workspaceRoot);
211
+ const mcpServer = resolveCogmemMcpServer(workspaceRoot);
142
212
  return [
143
213
  'mcp_servers:',
144
214
  ' cogmem:',
145
- ` command: "${mcpBin}"`,
146
- ' args: []',
215
+ ` command: "${mcpServer.command}"`,
216
+ ` args: ${JSON.stringify(mcpServer.args)}`,
147
217
  ' enabled: true',
148
218
  ' tools:',
149
219
  ' include:',
150
220
  ...HERMES_COGMEM_TOOLS.map((tool) => ` - ${tool}`),
151
221
  ].join('\n');
152
222
  }
153
- function resolveCogmemMcpCommand(workspaceRoot) {
223
+ function resolveCogmemMcpServer(workspaceRoot) {
154
224
  if (process.env.COGMEM_MCP_BIN)
155
- return process.env.COGMEM_MCP_BIN;
156
- const workspaceBin = join(workspaceRoot, 'node_modules', '.bin', 'cogmem-mcp');
157
- if (existsSync(workspaceBin))
158
- return workspaceBin;
225
+ return { command: process.env.COGMEM_MCP_BIN, args: [] };
226
+ if (process.env.COGMEM_BIN)
227
+ return { command: process.env.COGMEM_BIN, args: ['mcp'] };
228
+ const workspaceCli = join(workspaceRoot, 'node_modules', '.bin', 'cogmem');
229
+ if (existsSync(workspaceCli))
230
+ return { command: workspaceCli, args: ['mcp'] };
159
231
  const pathValue = process.env.PATH || '';
232
+ for (const segment of pathValue.split(':')) {
233
+ if (!segment)
234
+ continue;
235
+ const candidate = join(segment, 'cogmem');
236
+ if (existsSync(candidate))
237
+ return { command: candidate, args: ['mcp'] };
238
+ }
239
+ const workspaceMcp = join(workspaceRoot, 'node_modules', '.bin', 'cogmem-mcp');
240
+ if (existsSync(workspaceMcp))
241
+ return { command: workspaceMcp, args: [] };
160
242
  for (const segment of pathValue.split(':')) {
161
243
  if (!segment)
162
244
  continue;
163
245
  const candidate = join(segment, 'cogmem-mcp');
164
246
  if (existsSync(candidate))
165
- return candidate;
247
+ return { command: candidate, args: [] };
166
248
  }
167
- return 'cogmem-mcp';
249
+ return { command: 'cogmem', args: ['mcp'] };
168
250
  }
169
251
  function installSkill(args) {
170
252
  if (!args.agent)
@@ -227,6 +309,7 @@ function installSkill(args) {
227
309
  installed: !args.dryRun && !alreadyCurrent,
228
310
  alreadyCurrent,
229
311
  nextCommands: nextCommands(args.agent),
312
+ nextSteps: nextSteps(args.agent),
230
313
  hostConfigSnippet: hostConfigSnippet(args.agent, args.workspaceRoot, args.auto),
231
314
  autoMemory,
232
315
  hermesMcp,
@@ -237,9 +320,9 @@ function defaultHermesConfigPath(env = process.env) {
237
320
  }
238
321
  function installHermesMcpConfig(input) {
239
322
  const configPath = resolve(input.configPath || defaultHermesConfigPath());
240
- const serverCommand = resolveCogmemMcpCommand(resolve(input.workspaceRoot));
323
+ const server = resolveCogmemMcpServer(resolve(input.workspaceRoot));
241
324
  const original = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
242
- const patched = patchHermesMcpConfig(original, serverCommand);
325
+ const patched = patchHermesMcpConfig(original, server.command, server.args);
243
326
  const changed = patched !== original;
244
327
  let backupPath;
245
328
  if (!input.dryRun && (changed || input.force)) {
@@ -253,21 +336,22 @@ function installHermesMcpConfig(input) {
253
336
  return {
254
337
  enabled: true,
255
338
  configPath,
256
- serverCommand,
339
+ serverCommand: server.command,
340
+ serverArgs: server.args,
257
341
  dryRun: input.dryRun,
258
342
  configUpdated: changed || input.force,
259
343
  backupPath,
260
344
  };
261
345
  }
262
- function patchHermesMcpConfig(original, serverCommand) {
263
- if (/^\s+cogmem\s*:/m.test(original) && original.includes('cogmem-mcp')) {
346
+ function patchHermesMcpConfig(original, serverCommand, serverArgs) {
347
+ if (/^\s+cogmem\s*:/m.test(original)) {
264
348
  return patchExistingHermesCogmemConfig(original);
265
349
  }
266
350
  const lines = original.replace(/\r\n/g, '\n').split('\n');
267
351
  const serverBlock = [
268
352
  ' cogmem:',
269
353
  ` command: "${serverCommand}"`,
270
- ' args: []',
354
+ ` args: ${JSON.stringify(serverArgs)}`,
271
355
  ' enabled: true',
272
356
  ' tools:',
273
357
  ' include:',
@@ -366,7 +450,7 @@ function printHuman(result) {
366
450
  console.log('');
367
451
  console.log('Hermes MCP integration:');
368
452
  console.log(` config: ${result.hermesMcp.configPath}`);
369
- console.log(` command: ${result.hermesMcp.serverCommand}`);
453
+ console.log(` command: ${result.hermesMcp.serverCommand} ${result.hermesMcp.serverArgs.join(' ')}`.trimEnd());
370
454
  if (result.hermesMcp.backupPath)
371
455
  console.log(` backup: ${result.hermesMcp.backupPath}`);
372
456
  console.log(' reload: /reload-mcp');
@@ -22,6 +22,7 @@ export interface AgentImportResult {
22
22
  recordsIngested: number;
23
23
  skippedRecords: number;
24
24
  rawRecordsAnchored?: number;
25
+ emptyEpisodesSkipped?: number;
25
26
  reindexRaw?: boolean;
26
27
  processedSourceIds: string[];
27
28
  diagnostics: SourceAdapterDiagnostic[];
@@ -152,6 +152,7 @@ function previewSources(input) {
152
152
  recordsIngested: 0,
153
153
  skippedRecords: 0,
154
154
  rawRecordsAnchored: 0,
155
+ emptyEpisodesSkipped: 0,
155
156
  reindexRaw: false,
156
157
  processedSourceIds: [],
157
158
  diagnostics,
@@ -161,6 +162,7 @@ function previewSources(input) {
161
162
  async function importSources(input) {
162
163
  const opened = openKernel(input.args, input.workspaceRoot);
163
164
  const importedEpisodeIds = new Set();
165
+ let emptyEpisodesSkipped = 0;
164
166
  const processor = new InstalledBatchProcessor({
165
167
  cursorStore: opened.kernel.cursorStore,
166
168
  ingestBatch: async (items) => {
@@ -190,7 +192,7 @@ async function importSources(input) {
190
192
  for (const episodeId of importedEpisodeIds) {
191
193
  const episode = opened.kernel.getEpisode(episodeId);
192
194
  if (episode?.status !== 'sealed') {
193
- opened.kernel.sealImportedEpisode(episodeId, { reason: `${input.agent}_import_batch_boundary` });
195
+ emptyEpisodesSkipped += sealImportedEpisodeIfReady(opened.kernel, episodeId, `${input.agent}_import_batch_boundary`) ? 0 : 1;
194
196
  }
195
197
  }
196
198
  return {
@@ -207,6 +209,7 @@ async function importSources(input) {
207
209
  recordsIngested: summary.recordsIngested,
208
210
  skippedRecords: summary.skippedRecords,
209
211
  rawRecordsAnchored: summary.recordsIngested,
212
+ emptyEpisodesSkipped,
210
213
  reindexRaw: false,
211
214
  processedSourceIds: summary.processedSourceIds,
212
215
  diagnostics: summary.adapterDiagnostics,
@@ -238,6 +241,7 @@ async function reindexRawSources(input) {
238
241
  let rawRecordsAnchored = 0;
239
242
  let skippedRecords = 0;
240
243
  const importedEpisodeIds = new Set();
244
+ let emptyEpisodesSkipped = 0;
241
245
  try {
242
246
  for (const source of input.sources) {
243
247
  const adapter = adapters.get(source.adapterKind);
@@ -279,7 +283,7 @@ async function reindexRawSources(input) {
279
283
  for (const episodeId of importedEpisodeIds) {
280
284
  const episode = opened.kernel.getEpisode(episodeId);
281
285
  if (episode?.status !== 'sealed') {
282
- opened.kernel.sealImportedEpisode(episodeId, { reason: `${input.agent}_reindex_batch_boundary` });
286
+ emptyEpisodesSkipped += sealImportedEpisodeIfReady(opened.kernel, episodeId, `${input.agent}_reindex_batch_boundary`) ? 0 : 1;
283
287
  }
284
288
  }
285
289
  return {
@@ -297,6 +301,7 @@ async function reindexRawSources(input) {
297
301
  recordsIngested: rawRecordsAnchored,
298
302
  skippedRecords,
299
303
  rawRecordsAnchored,
304
+ emptyEpisodesSkipped,
300
305
  processedSourceIds,
301
306
  diagnostics,
302
307
  sourceResults,
@@ -307,6 +312,12 @@ async function reindexRawSources(input) {
307
312
  opened.kernel.close();
308
313
  }
309
314
  }
315
+ function sealImportedEpisodeIfReady(kernel, episodeId, reason) {
316
+ if (kernel.listEpisodeEventLinks(episodeId).length === 0)
317
+ return false;
318
+ kernel.sealImportedEpisode(episodeId, { reason });
319
+ return true;
320
+ }
310
321
  async function recordRawImportedEvidence(kernel, projectId, envelope) {
311
322
  const sourceRef = envelope.ingestInput.sourceRefs?.[0];
312
323
  const record = envelope.record;
@@ -483,6 +494,8 @@ function printHumanSummary(result) {
483
494
  console.log(`records ${action}: ${result.dryRun ? result.recordsWouldIngest : result.recordsIngested}`);
484
495
  if (result.rawRecordsAnchored !== undefined)
485
496
  console.log(`raw ledger anchors: ${result.rawRecordsAnchored}`);
497
+ if (result.emptyEpisodesSkipped)
498
+ console.log(`empty episodes skipped: ${result.emptyEpisodesSkipped}`);
486
499
  console.log(`records skipped: ${result.skippedRecords}`);
487
500
  if (result.diagnostics.length > 0) {
488
501
  console.log('diagnostics:');
package/dist/bin/mcp.js CHANGED
@@ -29,7 +29,8 @@ function stringArg(values, key) {
29
29
  }
30
30
  function usage() {
31
31
  return [
32
- 'Usage: cogmem-mcp [--db <memory.db>|--config <config.toml>] [--cwd <dir>]',
32
+ 'Usage: cogmem mcp [--db <memory.db>|--config <config.toml>] [--cwd <dir>]',
33
+ ' cogmem-mcp [--db <memory.db>|--config <config.toml>] [--cwd <dir>]',
33
34
  '',
34
35
  'Starts a stdio MCP server exposing recall, strategy, episode append/import/status/seal, conditional dream tick/status, memory map, maintenance, and prospective-memory tools.',
35
36
  ].join('\n');
@@ -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
  }
@@ -607,12 +780,24 @@ function runCandidates(kernel, args) {
607
780
  };
608
781
  }
609
782
  function printHuman(command, payload) {
610
- if (command === 'status') {
783
+ if (command === 'status' || command === 'plan') {
784
+ if (command === 'plan') {
785
+ console.log(`healthy: ${payload.healthy}`);
786
+ console.log(`queueSummary: ${JSON.stringify(payload.queueSummary)}`);
787
+ console.log(`nextActions: ${JSON.stringify(payload.nextActions)}`);
788
+ console.log(`blocking: ${JSON.stringify(payload.blocking)}`);
789
+ console.log(`nonBlocking: ${JSON.stringify(payload.nonBlocking)}`);
790
+ return;
791
+ }
611
792
  console.log(`rawEvents: ${payload.rawEventCount}`);
612
793
  console.log(`vectors: ${payload.vectorCount}`);
613
794
  console.log(`dreamBacklog: ${JSON.stringify(payload.dreamBacklog)}`);
614
795
  console.log(`episodeDream: ${JSON.stringify(payload.episodeDream)}`);
615
796
  console.log(`dreamCandidateQueue: ${JSON.stringify(payload.dreamCandidateQueue)}`);
797
+ if (payload.queueSummary)
798
+ console.log(`queueSummary: ${JSON.stringify(payload.queueSummary)}`);
799
+ if (payload.nextActions)
800
+ console.log(`nextActions: ${JSON.stringify(payload.nextActions)}`);
616
801
  return;
617
802
  }
618
803
  if (command === 'dream') {
@@ -646,6 +831,18 @@ function printHuman(command, payload) {
646
831
  return;
647
832
  }
648
833
  if (command === 'candidates') {
834
+ if (payload.queueSummary)
835
+ console.log(`queueSummary: ${JSON.stringify(payload.queueSummary)}`);
836
+ const groups = payload.groups;
837
+ if (groups) {
838
+ for (const [group, values] of Object.entries(groups)) {
839
+ console.log(`${group}: ${values.length}`);
840
+ for (const candidate of values) {
841
+ console.log(`- ${candidate.candidateId} ${candidate.candidateType} ${candidate.status} confidence=${candidate.confidence}`);
842
+ }
843
+ }
844
+ return;
845
+ }
649
846
  const candidates = Array.isArray(payload.candidates) ? payload.candidates : [];
650
847
  for (const candidate of candidates) {
651
848
  console.log(`- ${candidate.candidateId} ${candidate.candidateType} ${candidate.status} confidence=${candidate.confidence}`);
@@ -741,7 +938,7 @@ async function main() {
741
938
  console.log(usage());
742
939
  return;
743
940
  }
744
- if (args.command === 'status' || args.command === 'candidates') {
941
+ if (args.command === 'status' || args.command === 'plan' || args.command === 'candidates') {
745
942
  runReadOnlyInspection(args);
746
943
  return;
747
944
  }
@@ -801,3 +998,6 @@ main().catch((error) => {
801
998
  console.error(error instanceof Error ? error.message : String(error));
802
999
  process.exit(1);
803
1000
  });
1001
+ function cliArg(value) {
1002
+ return /^[A-Za-z0-9._:/=@+-]+$/u.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
1003
+ }