codemem 0.20.10 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/commands/db.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2BpC,eAAO,MAAM,SAAS,SAEe,CAAC"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/commands/db.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4CpC,eAAO,MAAM,SAAS,SAEe,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAEN,KAAK,uBAAuB,EAG5B,MAAM,uBAAuB,CAAC;AAQ/B,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKhE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASjD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ9D;AAED,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,WAAW,EAAE,MAAM,GAAG,IAAI,GACxB,MAAM,GAAG,IAAI,CAGf;AAqLD,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,uBAAuB,EACnC,QAAQ,GAAE,MAAM,EAAqB,GACnC,MAAM,EAAE,CAgBV;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS9D;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAUpF;AAkRD,eAAO,MAAM,YAAY,SAuBtB,CAAC"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAEN,KAAK,uBAAuB,EAG5B,MAAM,uBAAuB,CAAC;AAQ/B,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKhE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASjD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ9D;AAED,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,WAAW,EAAE,MAAM,GAAG,IAAI,GACxB,MAAM,GAAG,IAAI,CAGf;AAqLD,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,uBAAuB,EACnC,QAAQ,GAAE,MAAM,EAAqB,GACnC,MAAM,EAAE,CAgBV;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS9D;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAUpF;AA6RD,eAAO,MAAM,YAAY,SAuBtB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_COORDINATOR_DB_PATH, MemoryStore, ObserverClient, RawEventSweeper, VERSION, backfillTagsText, backfillVectors, buildRawEventEnvelopeFromHook, connect, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorListJoinRequestsAction, coordinatorReviewJoinRequestAction, createCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fingerprintPublicKey, getRawEventStatus, importMemories, initDatabase, isEmbeddingDisabled, loadPublicKey, loadSqliteVec, rawEventsGate, readCodememConfigFile, readCoordinatorSyncConfig, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
2
+ import { DEFAULT_COORDINATOR_DB_PATH, MemoryStore, ObserverClient, RawEventSweeper, SyncRetentionRunner, VERSION, backfillTagsText, backfillVectors, buildRawEventEnvelopeFromHook, bulkPruneReplicationOpsByAgeCutoff, connect, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorListJoinRequestsAction, coordinatorReviewJoinRequestAction, createCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fingerprintPublicKey, getRawEventStatus, importMemories, initDatabase, isEmbeddingDisabled, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOps, rawEventsGate, readCodememConfigFile, readCoordinatorSyncConfig, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -197,6 +197,18 @@ function parseKindsCsv(value) {
197
197
  const kinds = value.split(",").map((kind) => kind.trim()).filter((kind) => kind.length > 0);
198
198
  return kinds.length > 0 ? kinds : void 0;
199
199
  }
200
+ function estimateReplicationOpsBytes(db) {
201
+ try {
202
+ const row = db.prepare(`SELECT COALESCE(SUM(pgsize), 0) AS total_bytes
203
+ FROM dbstat
204
+ WHERE name = 'replication_ops'
205
+ OR name LIKE 'idx_replication_ops_%'
206
+ OR name LIKE 'sqlite_autoindex_replication_ops_%'`).get();
207
+ return Number(row?.total_bytes ?? 0);
208
+ } catch {
209
+ return 0;
210
+ }
211
+ }
200
212
  var dbCommand = new Command("db").configureHelp(helpStyle).description("Database maintenance");
201
213
  dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("Verify the SQLite database is present and schema-ready").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action((opts) => {
202
214
  const result = initDatabase(opts.db ?? opts.dbPath);
@@ -208,6 +220,67 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("V
208
220
  p.intro("codemem db vacuum");
209
221
  p.log.success(`Vacuumed: ${result.path}`);
210
222
  p.outro(`Size: ${result.sizeBytes.toLocaleString()} bytes`);
223
+ })).addCommand(new Command("prune-replication-ops").configureHelp(helpStyle).description("Prune replication op history with approximate oldest-first retention, dry-run, and progress reporting").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--dry-run", "show current size/targets without deleting").option("--max-age-days <days>", "retention age threshold in days", "30").option("--max-size-mb <mb>", "target replication log budget in MB", "512").option("--batch-ops <n>", "max ops deleted per batch", "5000").option("--batch-runtime-ms <ms>", "max runtime per batch in ms", "2000").option("--vacuum", "run VACUUM explicitly after prune completes").action((opts) => {
224
+ const dbPath = resolveDbPath(opts.db ?? opts.dbPath);
225
+ const db = connect(dbPath);
226
+ let dbOpen = true;
227
+ try {
228
+ const maxAgeDays = Number.parseInt(opts.maxAgeDays, 10) || 30;
229
+ const maxSizeMb = Number.parseInt(opts.maxSizeMb, 10) || 512;
230
+ const batchOps = Number.parseInt(opts.batchOps, 10) || 5e3;
231
+ const batchRuntimeMs = Number.parseInt(opts.batchRuntimeMs, 10) || 2e3;
232
+ const beforeBytes = estimateReplicationOpsBytes(db);
233
+ p.intro("codemem db prune-replication-ops");
234
+ p.log.info(`Replication ops size: ${formatBytes(beforeBytes)}`);
235
+ p.log.info(`Policy: approximately keep <= ${maxSizeMb} MB and <= ${maxAgeDays} day old history via oldest-first chunk pruning`);
236
+ const agePlan = planReplicationOpsAgePrune(db, maxAgeDays, batchOps);
237
+ if (agePlan.candidate_ops > 0) p.log.info(`Age pass plan: ${agePlan.candidate_ops.toLocaleString()} ops, ~${formatBytes(agePlan.estimated_candidate_bytes)} in ~${agePlan.estimated_batches.toLocaleString()} batch(es), cutoff ${agePlan.cutoff_cursor}`);
238
+ else p.log.info("Age pass plan: no ops older than cutoff");
239
+ if (opts.dryRun) {
240
+ p.outro("Dry run only; no changes made");
241
+ return;
242
+ }
243
+ let totalDeleted = 0;
244
+ let batches = 0;
245
+ let lastFloor = null;
246
+ let afterBytes = beforeBytes;
247
+ const ageResult = bulkPruneReplicationOpsByAgeCutoff(db, maxAgeDays, batchOps);
248
+ totalDeleted += ageResult.deleted;
249
+ lastFloor = ageResult.retained_floor_cursor;
250
+ afterBytes = ageResult.estimated_bytes_after ?? beforeBytes;
251
+ if (ageResult.deleted > 0) {
252
+ batches += 1;
253
+ p.log.step(`Age pass batch: deleted ${ageResult.deleted.toLocaleString()} ops, remaining size ~${formatBytes(afterBytes)}`);
254
+ }
255
+ while (true) {
256
+ const result = pruneReplicationOps(db, {
257
+ maxAgeDays: 365e3,
258
+ maxSizeBytes: maxSizeMb * 1024 * 1024,
259
+ maxDeleteOps: batchOps,
260
+ maxRuntimeMs: batchRuntimeMs
261
+ });
262
+ batches += 1;
263
+ totalDeleted += result.deleted;
264
+ lastFloor = result.retained_floor_cursor;
265
+ afterBytes = result.estimated_bytes_after ?? estimateReplicationOpsBytes(db);
266
+ p.log.step(`Batch ${batches}: deleted ${result.deleted.toLocaleString()} ops, remaining size ~${formatBytes(afterBytes)}`);
267
+ if (result.deleted === 0 || !result.stopped_by_budget) break;
268
+ }
269
+ p.log.info(`Deleted ops: ${totalDeleted.toLocaleString()}`);
270
+ p.log.info(`Estimated replication ops size after prune: ${formatBytes(afterBytes)} (approximate)`);
271
+ if (lastFloor) p.log.info(`Retained floor: ${lastFloor}`);
272
+ if (opts.vacuum) {
273
+ p.log.step("Running VACUUM as requested...");
274
+ db.close();
275
+ dbOpen = false;
276
+ const vacuumed = vacuumDatabase(dbPath);
277
+ p.outro(`Done. VACUUM complete. File size is now ${formatBytes(vacuumed.sizeBytes)}.`);
278
+ return;
279
+ }
280
+ p.outro("Done. Retention is approximate oldest-first pruning. SQLite file size may not shrink until you run `codemem db vacuum` explicitly (or re-run this command with --vacuum).");
281
+ } finally {
282
+ if (dbOpen) db.close();
283
+ }
211
284
  })).addCommand(new Command("raw-events-status").configureHelp(helpStyle).description("Show pending raw-event backlog by source stream").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max rows to show", "25").option("--json", "output as JSON").action((opts) => {
212
285
  const result = getRawEventStatus(opts.db ?? opts.dbPath, Number.parseInt(opts.limit, 10) || 25);
213
286
  if (opts.json) {
@@ -1155,8 +1228,13 @@ async function startForegroundViewer(invocation) {
1155
1228
  const sweeper = new RawEventSweeper(store, { observer });
1156
1229
  sweeper.start();
1157
1230
  const syncAbort = new AbortController();
1231
+ const retentionAbort = new AbortController();
1158
1232
  const syncConfig = readCoordinatorSyncConfig(readCodememConfigFile());
1159
1233
  const syncEnabled = syncConfig.syncEnabled;
1234
+ const retentionRunner = new SyncRetentionRunner({
1235
+ dbPath: resolveDbPath(invocation.dbPath ?? void 0),
1236
+ signal: retentionAbort.signal
1237
+ });
1160
1238
  const syncRuntimeStatus = {
1161
1239
  phase: syncEnabled ? "starting" : "disabled",
1162
1240
  detail: syncEnabled ? "Waiting for viewer startup to finish" : "Sync is disabled"
@@ -1200,6 +1278,10 @@ async function startForegroundViewer(invocation) {
1200
1278
  p.log.success(`Listening on http://${info.address}:${info.port}`);
1201
1279
  p.log.info(`Database: ${dbPath}`);
1202
1280
  p.log.step("Raw event sweeper started");
1281
+ if (syncConfig.syncRetentionEnabled) {
1282
+ retentionRunner.start();
1283
+ p.log.step("Retention maintenance runner started");
1284
+ }
1203
1285
  if (syncEnabled) {
1204
1286
  const syncStartDelayMs = 3e3;
1205
1287
  p.log.step(`Sync daemon will start in background (${syncStartDelayMs / 1e3}s delay)`);
@@ -1243,7 +1325,9 @@ async function startForegroundViewer(invocation) {
1243
1325
  const shutdown = async () => {
1244
1326
  p.outro("shutting down");
1245
1327
  syncAbort.abort();
1328
+ retentionAbort.abort();
1246
1329
  await sweeper.stop();
1330
+ await retentionRunner.stop();
1247
1331
  await new Promise((resolve) => {
1248
1332
  let remaining = syncServer ? 2 : 1;
1249
1333
  const done = () => {