cogmem 3.6.2 → 3.6.4

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.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { dirname, join, resolve, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { loadCogmemConfig, resolveCogmemConfigPath } from '../config/CogmemConfig.js';
6
7
  import { printCliJson } from './CliJson.js';
7
8
  import { DEFAULT_NPM_PACKAGE, resolveLatestNpmSpec } from './update-release.js';
@@ -29,6 +30,8 @@ function readArgs(argv) {
29
30
  json: values.json === true,
30
31
  from: typeof values.from === 'string' ? values.from : 'latest',
31
32
  installHome: typeof values['install-home'] === 'string' ? values['install-home'] : undefined,
33
+ global: values.global === true,
34
+ localDev: values['local-dev'] === true,
32
35
  manager,
33
36
  configPath: typeof values.config === 'string' ? values.config : undefined,
34
37
  skipMigrate: values['skip-migrate'] === true,
@@ -42,7 +45,10 @@ function detectManager(cwd) {
42
45
  return 'pnpm';
43
46
  return 'npm';
44
47
  }
45
- function buildCommand(manager, spec) {
48
+ function buildCommand(target, spec) {
49
+ if (target.kind === 'npm_global')
50
+ return ['npm', 'install', '-g', `cogmem@${spec}`];
51
+ const manager = target.manager;
46
52
  if (manager === 'bun')
47
53
  return ['bun', 'add', `cogmem@${spec}`];
48
54
  if (manager === 'pnpm')
@@ -52,18 +58,18 @@ function buildCommand(manager, spec) {
52
58
  function localCogmemBin(cwd) {
53
59
  return join(cwd, 'node_modules', '.bin', 'cogmem');
54
60
  }
55
- function buildMigrationExec(targetCwd, configPath) {
61
+ function buildMigrationExecForTarget(target, configPath) {
56
62
  return [
57
- localCogmemBin(targetCwd),
63
+ target.bin,
58
64
  'migrate',
59
65
  '--yes',
60
66
  '--backup',
61
67
  ...(configPath ? ['--config', configPath] : []),
62
68
  ];
63
69
  }
64
- function buildOpenClawRepairExec(targetCwd, configPath, workspaceRoot) {
70
+ function buildOpenClawRepairExecForTarget(target, configPath, workspaceRoot) {
65
71
  return [
66
- localCogmemBin(targetCwd),
72
+ target.bin,
67
73
  'doctor',
68
74
  '--fix',
69
75
  '--agent',
@@ -99,16 +105,57 @@ function shouldUpdateCwd(cwd) {
99
105
  const manifest = readPackageManifest(cwd);
100
106
  return manifest?.name === 'cogmem' || installedSpec(cwd) !== undefined;
101
107
  }
102
- function resolveUpdateCwd(args, env) {
108
+ function isCogmemSourceCheckout(cwd) {
109
+ const manifest = readPackageManifest(cwd);
110
+ return manifest?.name === 'cogmem'
111
+ && existsSync(join(cwd, 'src', 'bin', 'update.ts'))
112
+ && existsSync(join(cwd, '.git'));
113
+ }
114
+ function ownPackageRoot() {
115
+ return resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
116
+ }
117
+ function isLikelyNpmGlobalInstall(env) {
118
+ if (env.COGMEM_INSTALL_KIND === 'npm_global')
119
+ return true;
120
+ const root = ownPackageRoot();
121
+ const marker = `${sep}node_modules${sep}cogmem`;
122
+ if (!root.includes(marker))
123
+ return false;
124
+ const cwd = resolve(process.cwd());
125
+ return !cwd.startsWith(root + sep);
126
+ }
127
+ function resolveUpdateTarget(args, env) {
103
128
  const cwd = process.cwd();
104
- if (args.installHome)
105
- return args.installHome;
106
- if (shouldUpdateCwd(cwd))
107
- return cwd;
129
+ if (args.global || isLikelyNpmGlobalInstall(env)) {
130
+ return { cwd, kind: 'npm_global', manager: 'npm', bin: 'cogmem' };
131
+ }
132
+ if (args.installHome) {
133
+ const targetCwd = args.installHome;
134
+ return { cwd: targetCwd, kind: 'install_home', manager: args.manager || detectManager(targetCwd), bin: localCogmemBin(targetCwd) };
135
+ }
136
+ if (isCogmemSourceCheckout(cwd)) {
137
+ return {
138
+ cwd,
139
+ kind: 'source_checkout',
140
+ manager: args.manager || detectManager(cwd),
141
+ bin: localCogmemBin(cwd),
142
+ warning: args.localDev ? undefined : 'source_checkout_requires_local_dev_for_write',
143
+ };
144
+ }
145
+ if (shouldUpdateCwd(cwd)) {
146
+ return { cwd, kind: 'local_project', manager: args.manager || detectManager(cwd), bin: localCogmemBin(cwd) };
147
+ }
108
148
  const installHome = defaultInstallHome(env);
109
- if (existsSync(join(installHome, 'package.json')))
110
- return installHome;
111
- return cwd;
149
+ if (existsSync(join(installHome, 'package.json'))) {
150
+ return { cwd: installHome, kind: 'install_home', manager: args.manager || detectManager(installHome), bin: localCogmemBin(installHome) };
151
+ }
152
+ return {
153
+ cwd,
154
+ kind: 'cwd_fallback',
155
+ manager: args.manager || detectManager(cwd),
156
+ bin: localCogmemBin(cwd),
157
+ warning: 'no_install_home_or_project_dependency_detected',
158
+ };
112
159
  }
113
160
  function loadConfigForUpdate(args) {
114
161
  const resolution = resolveCogmemConfigPath({ configPath: args.configPath, cwd: process.cwd() });
@@ -136,18 +183,24 @@ async function main() {
136
183
  const resolvedSpec = args.from === 'latest'
137
184
  ? resolveLatestNpmSpec({ env: process.env })
138
185
  : args.from;
139
- const targetCwd = resolveUpdateCwd(args, process.env);
140
- const manager = args.manager || detectManager(targetCwd);
141
- const command = buildCommand(manager, resolvedSpec);
186
+ const target = resolveUpdateTarget(args, process.env);
187
+ if (!args.dryRun && target.kind === 'source_checkout' && !args.localDev) {
188
+ throw new Error('Refusing to update the Cogmem source checkout. Pass --local-dev for an intentional development update, --global for npm global, or --install-home <dir> for the one-line installer home.');
189
+ }
190
+ if (!args.dryRun && target.kind === 'cwd_fallback') {
191
+ throw new Error('Unable to find an installed Cogmem package to update. Pass --global for npm global installs or --install-home <dir> for the one-line installer home.');
192
+ }
193
+ const manager = target.manager;
194
+ const command = buildCommand(target, resolvedSpec);
142
195
  const config = loadConfigForUpdate(args);
143
196
  const migrationExec = !args.skipMigrate && config.configPath
144
- ? buildMigrationExec(targetCwd, config.configPath)
197
+ ? buildMigrationExecForTarget(target, config.configPath)
145
198
  : undefined;
146
199
  const openclawWorkspace = config.loaded?.integrations.openclaw.enabled
147
200
  ? config.loaded.integrations.openclaw.workspaceDir || process.cwd()
148
201
  : undefined;
149
202
  const openclawRepairExec = !args.skipAgentRefresh && openclawWorkspace && config.configPath
150
- ? buildOpenClawRepairExec(targetCwd, config.configPath, openclawWorkspace)
203
+ ? buildOpenClawRepairExecForTarget(target, config.configPath, openclawWorkspace)
151
204
  : undefined;
152
205
  const restartRequired = [
153
206
  ...(openclawWorkspace ? ['restart OpenClaw gateway or agent host'] : []),
@@ -157,13 +210,15 @@ async function main() {
157
210
  command: 'update',
158
211
  dryRun: args.dryRun,
159
212
  manager,
213
+ installKind: target.kind,
160
214
  from: args.from,
161
215
  source: 'npm',
162
216
  npmPackage: DEFAULT_NPM_PACKAGE,
163
217
  packageSpec: resolvedSpec,
164
- targetCwd,
165
- currentSpec: installedSpec(targetCwd),
218
+ targetCwd: target.cwd,
219
+ currentSpec: target.kind === 'npm_global' ? 'npm:global' : installedSpec(target.cwd),
166
220
  nextCommand: command.join(' '),
221
+ updateWarning: target.warning,
167
222
  configPath: config.configPath,
168
223
  migrationCommand: migrationExec?.join(' '),
169
224
  migrationSkippedReason: migrationExec ? undefined : (args.skipMigrate ? 'skip_migrate_flag' : config.skippedReason),
@@ -182,6 +237,8 @@ async function main() {
182
237
  else {
183
238
  console.log(`cogmem update ${args.dryRun ? 'dry-run' : 'running'}`);
184
239
  console.log(`target: ${result.targetCwd}`);
240
+ if (result.updateWarning)
241
+ console.log(`warning: ${result.updateWarning}`);
185
242
  console.log(`current: ${result.currentSpec || 'not listed in package.json'}`);
186
243
  console.log(`command: ${result.nextCommand}`);
187
244
  if (result.migrationCommand)
@@ -191,7 +248,7 @@ async function main() {
191
248
  console.log(result.followUp);
192
249
  }
193
250
  if (!args.dryRun) {
194
- const updateExitCode = await runCommand(command, targetCwd);
251
+ const updateExitCode = await runCommand(command, target.cwd);
195
252
  if (updateExitCode !== 0)
196
253
  process.exit(updateExitCode);
197
254
  if (migrationExec) {
@@ -54,6 +54,7 @@ export declare class EpisodeStore {
54
54
  }): EpisodeEventLink;
55
55
  getEventLink(eventId: string): EpisodeEventLink | undefined;
56
56
  listEventLinks(episodeId: string): EpisodeEventLink[];
57
+ isEpisodeEmpty(episodeId: string): boolean;
57
58
  addCrossReference(input: {
58
59
  projectId: string;
59
60
  episodeId: string;
@@ -120,6 +121,10 @@ export declare class EpisodeStore {
120
121
  maxAttempts: number;
121
122
  runId?: string;
122
123
  }): ClaimedEpisodeDreamJob[];
124
+ skipEmptyDreamJobs(input: {
125
+ projectId?: string;
126
+ now?: number;
127
+ }): number;
123
128
  completeDreamJob(episodeId: string, leaseId: string, candidateIds: string[], now: number): void;
124
129
  failDreamJob(episodeId: string, leaseId: string, error: string, input: {
125
130
  now: number;
@@ -128,6 +133,8 @@ export declare class EpisodeStore {
128
133
  retryAfter?: number;
129
134
  }): void;
130
135
  retryFailed(projectId?: string): number;
136
+ private markEmptyEpisodeDreamSkipped;
137
+ private markEmptyEpisodeDreamSkippedMany;
131
138
  getDreamStatus(projectId?: string): EpisodeDreamStatus;
132
139
  countUnassignedRawEvents(projectId?: string): number;
133
140
  markEventDisposition(input: {
@@ -142,6 +142,12 @@ export class EpisodeStore {
142
142
  SELECT * FROM memory_episode_events WHERE episode_id = ? ORDER BY position
143
143
  `).all(episodeId).map(mapEventLink);
144
144
  }
145
+ isEpisodeEmpty(episodeId) {
146
+ const episode = this.getEpisode(episodeId);
147
+ if (!episode)
148
+ throw new Error(`episode_not_found:${episodeId}`);
149
+ return this.listEventLinks(episodeId).length === 0;
150
+ }
145
151
  addCrossReference(input) {
146
152
  const episode = this.getEpisode(input.episodeId);
147
153
  if (!episode)
@@ -334,9 +340,18 @@ export class EpisodeStore {
334
340
  const now = input.now ?? Date.now();
335
341
  const episodes = this.listEpisodes({ projectId: input.projectId, statuses: ['soft_sealed'], limit: 1000 })
336
342
  .filter((episode) => (episode.sealedAt || episode.updatedAt) <= input.sealedBefore);
337
- for (const episode of episodes)
343
+ let sealed = 0;
344
+ for (const episode of episodes) {
345
+ if (this.isEpisodeEmpty(episode.episodeId)) {
346
+ if (episode.dreamError !== 'episode_empty_soft_seal_not_promoted') {
347
+ this.markEmptyEpisodeDreamSkipped(episode.episodeId, now, 'episode_empty_soft_seal_not_promoted');
348
+ }
349
+ continue;
350
+ }
338
351
  this.sealEpisode(episode.episodeId, { mode: 'hard', reason: 'soft_seal_stabilized', now });
339
- return episodes.length;
352
+ sealed += 1;
353
+ }
354
+ return sealed;
340
355
  }
341
356
  claimDreamJobs(input) {
342
357
  this.db.prepare(`
@@ -372,15 +387,21 @@ export class EpisodeStore {
372
387
  UPDATE memory_episodes SET dream_status = 'queued', dream_error = NULL
373
388
  WHERE episode_id IN (SELECT episode_id FROM episode_dream_jobs WHERE state = 'retry_scheduled' AND updated_at = ?)
374
389
  `).run(input.now);
375
- const where = [`(state = 'pending' OR (state = 'retry_scheduled' AND attempts < ?))`];
390
+ this.skipEmptyDreamJobs({ projectId: input.projectId, now: input.now });
391
+ const where = [`(j.state = 'pending' OR (j.state = 'retry_scheduled' AND j.attempts < ?))`];
376
392
  const params = [input.maxAttempts];
377
393
  if (input.projectId) {
378
- where.push('project_id = ?');
394
+ where.push('j.project_id = ?');
379
395
  params.push(input.projectId);
380
396
  }
381
397
  const rows = this.db.prepare(`
382
- SELECT episode_id, project_id, mode_hint, attempts, created_at FROM episode_dream_jobs
383
- WHERE ${where.join(' AND ')} ORDER BY priority DESC, created_at LIMIT ?
398
+ SELECT j.episode_id, j.project_id, j.mode_hint, j.attempts, j.created_at
399
+ FROM episode_dream_jobs j
400
+ JOIN memory_episodes e ON e.episode_id = j.episode_id
401
+ WHERE ${where.join(' AND ')}
402
+ AND e.event_count > 0
403
+ AND EXISTS (SELECT 1 FROM memory_episode_events ee WHERE ee.episode_id = j.episode_id)
404
+ ORDER BY j.priority DESC, j.created_at LIMIT ?
384
405
  `).all(...params, Math.max(1, Math.min(Math.trunc(input.limit), 100)));
385
406
  const claimed = [];
386
407
  for (const row of rows) {
@@ -403,6 +424,32 @@ export class EpisodeStore {
403
424
  }
404
425
  return claimed;
405
426
  }
427
+ skipEmptyDreamJobs(input) {
428
+ const now = input.now ?? Date.now();
429
+ const params = [];
430
+ const where = [
431
+ `j.state IN ('pending', 'failed_retryable', 'retry_scheduled')`,
432
+ `(COALESCE(e.event_count, 0) = 0 OR NOT EXISTS (
433
+ SELECT 1 FROM memory_episode_events ee WHERE ee.episode_id = j.episode_id
434
+ ))`,
435
+ ];
436
+ if (input.projectId) {
437
+ where.push(`j.project_id = ?`);
438
+ params.push(input.projectId);
439
+ }
440
+ const rows = this.db.prepare(`
441
+ SELECT j.episode_id FROM episode_dream_jobs j
442
+ LEFT JOIN memory_episodes e ON e.episode_id = j.episode_id
443
+ WHERE ${where.join(' AND ')}
444
+ ORDER BY j.updated_at DESC
445
+ LIMIT 1000
446
+ `).all(...params);
447
+ const episodeIds = rows.map((row) => row.episode_id);
448
+ if (!episodeIds.length)
449
+ return 0;
450
+ this.markEmptyEpisodeDreamSkippedMany(episodeIds, now, 'episode_empty_skipped_no_raw_evidence');
451
+ return episodeIds.length;
452
+ }
406
453
  completeDreamJob(episodeId, leaseId, candidateIds, now) {
407
454
  const result = this.db.prepare(`
408
455
  UPDATE episode_dream_jobs SET state = 'processed', candidate_ids_json = ?, lease_id = NULL,
@@ -441,6 +488,28 @@ export class EpisodeStore {
441
488
  }
442
489
  return Number(result.changes || 0);
443
490
  }
491
+ markEmptyEpisodeDreamSkipped(episodeId, now, reason) {
492
+ this.markEmptyEpisodeDreamSkippedMany([episodeId], now, reason);
493
+ }
494
+ markEmptyEpisodeDreamSkippedMany(episodeIds, now, reason) {
495
+ if (!episodeIds.length)
496
+ return;
497
+ const placeholders = episodeIds.map(() => '?').join(', ');
498
+ this.db.transaction(() => {
499
+ this.db.prepare(`
500
+ UPDATE episode_dream_jobs SET state = 'skipped', lease_id = NULL, lease_until = NULL,
501
+ retry_after = NULL, failure_category = 'episode_empty', last_error = ?, candidate_ids_json = '[]',
502
+ updated_at = ?
503
+ WHERE episode_id IN (${placeholders})
504
+ AND state IN ('pending', 'processing', 'failed_retryable', 'retry_scheduled')
505
+ `).run(reason, now, ...episodeIds);
506
+ this.db.prepare(`
507
+ UPDATE memory_episodes SET dream_status = 'failed', dream_error = ?, last_dream_run_id = NULL,
508
+ dream_candidate_count = 0, updated_at = ?
509
+ WHERE episode_id IN (${placeholders})
510
+ `).run(reason, now, ...episodeIds);
511
+ })();
512
+ }
444
513
  getDreamStatus(projectId) {
445
514
  const rows = (projectId
446
515
  ? this.db.prepare(`SELECT state, COUNT(*) AS count FROM episode_dream_jobs WHERE project_id = ? GROUP BY state`).all(projectId)
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.1';
86
+ const CORE_VERSION = '3.6.4';
87
87
  const LATEST_SCHEMA_VERSION = 27;
88
88
  export class MemoryKernel {
89
89
  options;
@@ -983,6 +983,8 @@ export class MemoryKernel {
983
983
  }
984
984
  sealImportedEpisode(episodeId, input) {
985
985
  const links = this.episodeStore.listEventLinks(episodeId);
986
+ if (!links.length)
987
+ throw new Error(`episode_empty:${episodeId}`);
986
988
  const averageConfidence = links.length
987
989
  ? links.reduce((total, link) => total + link.confidence, 0) / links.length
988
990
  : 0;
@@ -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.2';
8
+ const PLUGIN_VERSION = '0.6.3';
9
9
  function defaultPublicEntrypoint() {
10
10
  return join(resolve(dirname(fileURLToPath(import.meta.url)), '../..'), 'public.js');
11
11
  }
@@ -735,6 +735,7 @@ function bridgeConfig(config) {
735
735
  rememberQueuePath: rememberQueuePath(config),
736
736
  rememberMaxAttempts: config.rememberMaxAttempts || 3,
737
737
  rememberDrainBatchSize: config.rememberDrainBatchSize || 20,
738
+ rememberDrainTimeoutMs: config.rememberDrainTimeoutMs || 60000,
738
739
  };
739
740
  }
740
741
 
@@ -988,7 +989,7 @@ function audit(config, record) {
988
989
  const plugin = {
989
990
  id: PLUGIN_ID,
990
991
  name: 'CogMem Auto Memory',
991
- version: '0.6.2',
992
+ version: '0.6.3',
992
993
  register(api) {
993
994
  if (!api || typeof api.on !== 'function') {
994
995
  throw new Error('OpenClaw plugin API missing api.on');
@@ -1231,7 +1232,7 @@ module.exports.__testing = {
1231
1232
  }
1232
1233
  function pluginBridgeMjs() {
1233
1234
  return String.raw `#!/usr/bin/env bun
1234
- import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
1235
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
1235
1236
  import { dirname, join } from 'node:path';
1236
1237
  import { pathToFileURL } from 'node:url';
1237
1238
 
@@ -1581,7 +1582,8 @@ function acquireRememberQueueLock(bridgeConfig) {
1581
1582
  if (!queuePath) throw new Error('missing rememberQueuePath');
1582
1583
  mkdirSync(dirname(queuePath), { recursive: true });
1583
1584
  const lockPath = queuePath + '.lock';
1584
- if (!existsSync(queuePath)) return { acquired: false, locked: false, empty: true, lockPath };
1585
+ const recoveredProcessing = recoverStaleProcessingFiles(bridgeConfig);
1586
+ if (!existsSync(queuePath)) return { acquired: false, locked: false, empty: true, lockPath, recoveredProcessing };
1585
1587
  const timeoutMs = Number(bridgeConfig.rememberDrainTimeoutMs || 60000);
1586
1588
  try {
1587
1589
  mkdirSync(lockPath);
@@ -1602,12 +1604,12 @@ function acquireRememberQueueLock(bridgeConfig) {
1602
1604
  staleLockRecovered: true,
1603
1605
  queuePath,
1604
1606
  }) + '\n');
1605
- return { acquired: true, locked: false, empty: false, lockPath, staleRecovered: true };
1607
+ return { acquired: true, locked: false, empty: false, lockPath, staleRecovered: true, recoveredProcessing };
1606
1608
  }
1607
1609
  } catch {}
1608
1610
  return { acquired: false, locked: true, empty: false, lockPath };
1609
1611
  }
1610
- return { acquired: true, locked: false, empty: false, lockPath };
1612
+ return { acquired: true, locked: false, empty: false, lockPath, recoveredProcessing };
1611
1613
  }
1612
1614
 
1613
1615
  async function drainRememberQueueWithLock(bridgeConfig, queueLock, kernel, memory) {
@@ -1655,7 +1657,29 @@ async function drainRememberQueueWithLock(bridgeConfig, queueLock, kernel, memor
1655
1657
  } finally {
1656
1658
  if (queueLock.acquired) rmSync(queueLock.lockPath, { recursive: true, force: true });
1657
1659
  }
1658
- return { drained, failed, deferred, locked: false, staleRecovered: queueLock.staleRecovered === true };
1660
+ return { drained, failed, deferred, locked: false, staleRecovered: queueLock.staleRecovered === true, recoveredProcessing: queueLock.recoveredProcessing || 0 };
1661
+ }
1662
+
1663
+ function recoverStaleProcessingFiles(bridgeConfig) {
1664
+ const queuePath = bridgeConfig.rememberQueuePath;
1665
+ const dir = dirname(queuePath);
1666
+ const prefix = queuePath.split('/').pop() + '.';
1667
+ const timeoutMs = Number(bridgeConfig.rememberDrainTimeoutMs || 60000);
1668
+ let recovered = 0;
1669
+ if (!existsSync(dir)) return recovered;
1670
+ for (const entry of readdirSync(dir)) {
1671
+ if (!entry.startsWith(prefix) || !entry.endsWith('.processing')) continue;
1672
+ const path = join(dir, entry);
1673
+ try {
1674
+ const ageMs = Date.now() - statSync(path).mtimeMs;
1675
+ if (!Number.isFinite(ageMs) || ageMs < timeoutMs) continue;
1676
+ const lines = readFileSync(path, 'utf8').split('\n').map((line) => line.trim()).filter(Boolean);
1677
+ for (const line of lines) appendFileSync(queuePath, line + '\n');
1678
+ rmSync(path, { force: true });
1679
+ recovered += lines.length;
1680
+ } catch {}
1681
+ }
1682
+ return recovered;
1659
1683
  }
1660
1684
 
1661
1685
  function compactRecallItems(items, config) {
@@ -3,6 +3,9 @@ import type { Migration } from '../types/Migration.js';
3
3
  export interface SchemaMigrationRunOptions {
4
4
  dryRun?: boolean;
5
5
  }
6
+ export interface SchemaMigrationRunnerOptions {
7
+ readonly?: boolean;
8
+ }
6
9
  export interface SchemaMigrationResult {
7
10
  pending: string[];
8
11
  applied: string[];
@@ -12,10 +15,15 @@ export interface SchemaMigrationResult {
12
15
  export declare class SchemaMigrationRunner {
13
16
  private readonly db;
14
17
  private readonly migrations;
15
- constructor(db: Database, migrations: Migration[]);
18
+ private readonly options;
19
+ constructor(db: Database, migrations: Migration[], options?: SchemaMigrationRunnerOptions);
16
20
  plan(): Migration[];
17
21
  run(options?: SchemaMigrationRunOptions): SchemaMigrationResult;
18
22
  currentVersion(): string | undefined;
23
+ private appliedVersions;
24
+ private schemaMigrationsTableExists;
25
+ private legacySchemaVersion;
26
+ private legacyCurrentVersion;
19
27
  private adoptLegacyVersion;
20
28
  }
21
29
  //# sourceMappingURL=SchemaMigrationRunner.d.ts.map
@@ -1,9 +1,13 @@
1
1
  export class SchemaMigrationRunner {
2
2
  db;
3
3
  migrations;
4
- constructor(db, migrations) {
4
+ options;
5
+ constructor(db, migrations, options = {}) {
5
6
  this.db = db;
6
7
  this.migrations = migrations;
8
+ this.options = options;
9
+ if (this.options.readonly)
10
+ return;
7
11
  this.db.exec(`
8
12
  CREATE TABLE IF NOT EXISTS _schema_migrations (
9
13
  version TEXT PRIMARY KEY,
@@ -14,7 +18,7 @@ export class SchemaMigrationRunner {
14
18
  this.adoptLegacyVersion();
15
19
  }
16
20
  plan() {
17
- const applied = new Set(this.db.prepare(`SELECT version FROM _schema_migrations`).all().map((row) => row.version));
21
+ const applied = this.appliedVersions();
18
22
  return [...this.migrations]
19
23
  .sort((a, b) => a.version.localeCompare(b.version))
20
24
  .filter((migration) => !applied.has(migration.version));
@@ -39,20 +43,57 @@ export class SchemaMigrationRunner {
39
43
  return { pending: pending.map((item) => item.version), applied, currentVersion: this.currentVersion(), dryRun: false };
40
44
  }
41
45
  currentVersion() {
46
+ const legacyCurrent = this.legacyCurrentVersion();
47
+ if (!this.schemaMigrationsTableExists()) {
48
+ return legacyCurrent;
49
+ }
42
50
  const row = this.db.prepare(`
43
51
  SELECT version FROM _schema_migrations ORDER BY version DESC LIMIT 1
44
52
  `).get();
45
- return row?.version;
53
+ return [row?.version, legacyCurrent].filter((version) => Boolean(version)).sort((a, b) => b.localeCompare(a))[0];
46
54
  }
47
- adoptLegacyVersion() {
55
+ appliedVersions() {
56
+ const applied = new Set();
57
+ const legacyVersion = this.legacySchemaVersion();
58
+ for (const migration of this.migrations) {
59
+ if (legacyVersion !== undefined && Number.parseInt(migration.version, 10) <= legacyVersion) {
60
+ applied.add(migration.version);
61
+ }
62
+ }
63
+ if (!this.schemaMigrationsTableExists()) {
64
+ return applied;
65
+ }
66
+ for (const row of this.db.prepare(`SELECT version FROM _schema_migrations`).all()) {
67
+ applied.add(row.version);
68
+ }
69
+ return applied;
70
+ }
71
+ schemaMigrationsTableExists() {
72
+ return Boolean(this.db.prepare(`
73
+ SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_schema_migrations'
74
+ `).get());
75
+ }
76
+ legacySchemaVersion() {
48
77
  const metaExists = this.db.prepare(`
49
78
  SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_meta'
50
79
  `).get();
51
80
  if (!metaExists)
52
- return;
81
+ return undefined;
53
82
  const row = this.db.prepare(`SELECT value FROM _meta WHERE key = 'schema_version'`).get();
54
83
  const legacyVersion = Number.parseInt(row?.value || '', 10);
55
- if (!Number.isFinite(legacyVersion))
84
+ return Number.isFinite(legacyVersion) ? legacyVersion : undefined;
85
+ }
86
+ legacyCurrentVersion() {
87
+ const legacyVersion = this.legacySchemaVersion();
88
+ if (legacyVersion === undefined)
89
+ return undefined;
90
+ return [...this.migrations]
91
+ .filter((migration) => Number.parseInt(migration.version, 10) <= legacyVersion)
92
+ .sort((a, b) => b.version.localeCompare(a.version))[0]?.version;
93
+ }
94
+ adoptLegacyVersion() {
95
+ const legacyVersion = this.legacySchemaVersion();
96
+ if (legacyVersion === undefined)
56
97
  return;
57
98
  const insert = this.db.prepare(`
58
99
  INSERT OR IGNORE INTO _schema_migrations (version, description, applied_at)
@@ -15,19 +15,24 @@ This writes `~/.hermes/skills/cogmem-memory/SKILL.md`, which Hermes discovers as
15
15
  Run from the Hermes workspace root:
16
16
 
17
17
  ```bash
18
- COGMEM_SKIP_INIT=1 curl -fsSL https://raw.githubusercontent.com/liuqin164/cogmem/main/install.sh | bash
19
- cogmem init --yes --agent hermes
20
- cogmem doctor --fix --agent hermes --workspace .
21
- cogmem connect hermes --workspace . --auto
22
- cogmem connect hermes --workspace .
18
+ npm install cogmem@latest --save
19
+ COGMEM="./node_modules/.bin/cogmem"
20
+ "$COGMEM" doctor
21
+ "$COGMEM" connect hermes --workspace . --auto --force --json
22
+ ```
23
+
24
+ Do not run `cogmem init` as an unattended agent action. It is an interactive wizard. Use it only when an operator is present:
25
+
26
+ ```bash
27
+ "$COGMEM" init --agent hermes --scope project
23
28
  ```
24
29
 
25
- The default install creates:
30
+ The workspace install creates:
26
31
 
27
32
  ```text
28
- ~/.cogmem/config.toml
29
- ~/.cogmem/memory.db
30
- ~/.cogmem/snapshots/
33
+ .cogmem/config.toml
34
+ .cogmem/memory.db
35
+ .cogmem/snapshots/
31
36
  ```
32
37
 
33
38
  Use `~/.cogmem/config.toml` or a project `.cogmem/config.toml` as the stable configuration source. Do not create `.cogmem.env` files or pass `--env-path` for normal installs. Environment variables are only for explicit process-level overrides documented by the CLI, not for hidden workspace configuration.
@@ -44,7 +49,7 @@ Use MCP `cogmem_strategy_plan` when the agent needs to inspect the selected memo
44
49
 
45
50
  Prospective Memory is not executable instruction. Only a user-confirmed candidate may appear as due, and even then the agent must obtain normal host authorization before acting. Use `cogmem prospective` for state transitions and `cogmem brain-eval` for release validation.
46
51
 
47
- Use `cogmem init --yes --agent hermes --scope project` only when this workspace needs its own `.cogmem/` directory.
52
+ Use project-local config when this workspace owns its memory; use global config only when the operator intentionally shares one Cogmem backend across workspaces.
48
53
 
49
54
  To embed imported memories with a local quantized model, run Ollama locally and configure the kernel before importing:
50
55
 
@@ -117,14 +122,23 @@ cogmem normalize-transcript --input ./hermes-sessions.jsonl --output ./hermes.no
117
122
  cogmem import-hermes --workspace . --project hermes --session ./hermes.normalized.md
118
123
  ```
119
124
 
120
- After import, inspect the batch-sealed episodes, run one conditional curation tick, then invoke CPU governance separately:
125
+ Cogmem 3.6.4 skips import batch sealing for empty episode boundaries and skips legacy empty Dream jobs so one bad imported episode cannot block the queue.
126
+
127
+ After import, use this order:
121
128
 
122
129
  ```bash
130
+ cogmem memory status --project hermes --json
123
131
  cogmem episode status --project hermes --json
124
- cogmem dream tick --project hermes --mode auto --json
125
- cogmem memory govern --project hermes --json
132
+ cogmem dream status --project hermes --json
133
+ cogmem dream tick --project hermes --mode auto --max-episodes 20 --json
134
+ cogmem memory candidates --project hermes --status candidate --json
135
+ cogmem memory govern --project hermes --limit 100 --json
136
+ cogmem memory candidates --project hermes --status needs_confirmation --json
137
+ cogmem memory review --project hermes --id <candidate-id> --action approve --actor <operator> --reason "confirmed by user" --confirmation-event <user-event-id> --json
126
138
  ```
127
139
 
140
+ `needs_confirmation` is not the Dream backlog. `memory govern` does not approve it; use `memory review` with explicit evidence.
141
+
128
142
  ## Active Memory Search
129
143
 
130
144
  If the current prompt does not include enough Cogmem memory context, query Cogmem directly before searching legacy files: