codemem 0.26.0 → 0.26.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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, RawEventSweeper, RefBackfillRunner, SessionContextBackfillRunner, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, ensureDeviceIdentity, ensureSchemaBootstrapped, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingSessionContextBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, loadObserverConfig, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
2
+ import { DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, REF_BACKFILL_JOB, RawEventSweeper, RefBackfillRunner, SESSION_CONTEXT_BACKFILL_JOB, SessionContextBackfillRunner, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, ensureDeviceIdentity, ensureSchemaBootstrapped, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingSessionContextBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, loadObserverConfig, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
@@ -3564,6 +3564,19 @@ function readViewerPidRecord(dbPath) {
3564
3564
  }
3565
3565
  return null;
3566
3566
  }
3567
+ function normalizeViewerHost(host) {
3568
+ const normalized = host.trim().toLowerCase();
3569
+ if (normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]") return "loopback";
3570
+ return normalized;
3571
+ }
3572
+ async function findRuntimeViewerConflict(dbPath, target) {
3573
+ const record = readViewerPidRecord(dbPath);
3574
+ if (!record) return null;
3575
+ if (normalizeViewerHost(record.host) === normalizeViewerHost(target.host) && record.port === target.port) return null;
3576
+ if (!isProcessRunning(record.pid)) return null;
3577
+ if (!await respondsLikeCodememViewer(record)) return null;
3578
+ return record;
3579
+ }
3567
3580
  function isProcessRunning(pid) {
3568
3581
  try {
3569
3582
  process.kill(pid, 0);
@@ -3735,6 +3748,88 @@ async function startBackgroundViewer(invocation) {
3735
3748
  p.intro("codemem viewer");
3736
3749
  p.outro(`Viewer started in background (pid ${child.pid}) at http://${invocation.host}:${invocation.port}`);
3737
3750
  }
3751
+ function createSequentialBackfillCoordinator(store, jobPlans, signal) {
3752
+ const pollIntervalMs = 1e3;
3753
+ let activeRunner = null;
3754
+ let activePlan = null;
3755
+ let activePollTimer = null;
3756
+ let nextJobIndex = 0;
3757
+ let stopped = false;
3758
+ const clearPollTimer = () => {
3759
+ if (!activePollTimer) return;
3760
+ clearTimeout(activePollTimer);
3761
+ activePollTimer = null;
3762
+ };
3763
+ const schedulePoll = (fn) => {
3764
+ clearPollTimer();
3765
+ activePollTimer = setTimeout(fn, pollIntervalMs);
3766
+ if (typeof activePollTimer === "object" && "unref" in activePollTimer) activePollTimer.unref();
3767
+ };
3768
+ const waitForCurrentJob = () => {
3769
+ if (stopped || signal?.aborted || !activePlan || !activeRunner) return;
3770
+ const job = getMaintenanceJob(store.db, activePlan.kind);
3771
+ if (!activePlan.isPending(store.db)) {
3772
+ const finishedPlan = activePlan;
3773
+ const finishedRunner = activeRunner;
3774
+ activePlan = null;
3775
+ activeRunner = null;
3776
+ finishedRunner.stop().finally(() => {
3777
+ if (!stopped && !signal?.aborted) {
3778
+ p.log.step(`${finishedPlan.name} backfill complete`);
3779
+ startNextJob();
3780
+ }
3781
+ });
3782
+ return;
3783
+ }
3784
+ if (job?.status === "failed") {
3785
+ const failedPlan = activePlan;
3786
+ const failedRunner = activeRunner;
3787
+ activePlan = null;
3788
+ activeRunner = null;
3789
+ failedRunner.stop().finally(() => {
3790
+ if (!stopped && !signal?.aborted) {
3791
+ p.log.warn(`${failedPlan.name} backfill failed and will be retried on a later startup`);
3792
+ startNextJob();
3793
+ }
3794
+ });
3795
+ return;
3796
+ }
3797
+ schedulePoll(waitForCurrentJob);
3798
+ };
3799
+ const startNextJob = () => {
3800
+ clearPollTimer();
3801
+ if (stopped || signal?.aborted) return;
3802
+ while (nextJobIndex < jobPlans.length) {
3803
+ const plan = jobPlans[nextJobIndex++];
3804
+ if (!plan) continue;
3805
+ if (!plan.isPending(store.db)) continue;
3806
+ activePlan = plan;
3807
+ activeRunner = plan.createRunner();
3808
+ p.log.step(`${plan.name} backfill started`);
3809
+ activeRunner.start();
3810
+ schedulePoll(waitForCurrentJob);
3811
+ return;
3812
+ }
3813
+ p.log.step("All backfill jobs complete");
3814
+ };
3815
+ return {
3816
+ start: () => {
3817
+ if (stopped || signal?.aborted) return;
3818
+ const pendingCount = jobPlans.filter((plan) => plan.isPending(store.db)).length;
3819
+ if (pendingCount === 0) return;
3820
+ p.log.step(`${pendingCount} backfill job(s) pending — starting sequential runners`);
3821
+ startNextJob();
3822
+ },
3823
+ stop: async () => {
3824
+ stopped = true;
3825
+ clearPollTimer();
3826
+ const runner = activeRunner;
3827
+ activeRunner = null;
3828
+ activePlan = null;
3829
+ if (runner) await runner.stop();
3830
+ }
3831
+ };
3832
+ }
3738
3833
  async function startForegroundViewer(invocation) {
3739
3834
  const { createApp, createSyncApp, closeStore, getStore } = await import("@codemem/server");
3740
3835
  const { serve } = await import("@hono/node-server");
@@ -3764,16 +3859,44 @@ async function startForegroundViewer(invocation) {
3764
3859
  sweeper.start();
3765
3860
  const syncAbort = new AbortController();
3766
3861
  const retentionAbort = new AbortController();
3862
+ const backfillAbort = new AbortController();
3767
3863
  const syncConfig = readCoordinatorSyncConfig(invocation.configPath ? readCodememConfigFileAtPath(invocation.configPath) : readCodememConfigFile());
3768
3864
  const syncEnabled = syncConfig.syncEnabled;
3865
+ const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
3769
3866
  const retentionRunner = new SyncRetentionRunner({
3770
- dbPath: resolveDbPath(invocation.dbPath ?? void 0),
3867
+ dbPath,
3771
3868
  signal: retentionAbort.signal
3772
3869
  });
3773
- const vectorMigrationRunner = new VectorModelMigrationRunner({ dbPath: resolveDbPath(invocation.dbPath ?? void 0) });
3774
- const dedupKeyBackfillRunner = new DedupKeyBackfillRunner({ dbPath: resolveDbPath(invocation.dbPath ?? void 0) });
3775
- const sessionContextBackfillRunner = new SessionContextBackfillRunner({ dbPath: resolveDbPath(invocation.dbPath ?? void 0) });
3776
- const refBackfillRunner = new RefBackfillRunner({ dbPath: resolveDbPath(invocation.dbPath ?? void 0) });
3870
+ const vectorMigrationRunner = new VectorModelMigrationRunner({ dbPath });
3871
+ const backfillCoordinator = createSequentialBackfillCoordinator(store, [
3872
+ {
3873
+ name: "Dedup-key",
3874
+ kind: DEDUP_KEY_BACKFILL_JOB,
3875
+ isPending: hasPendingDedupKeyBackfill,
3876
+ createRunner: () => new DedupKeyBackfillRunner({
3877
+ dbPath,
3878
+ signal: backfillAbort.signal
3879
+ })
3880
+ },
3881
+ {
3882
+ name: "Session-context",
3883
+ kind: SESSION_CONTEXT_BACKFILL_JOB,
3884
+ isPending: hasPendingSessionContextBackfill,
3885
+ createRunner: () => new SessionContextBackfillRunner({
3886
+ dbPath,
3887
+ signal: backfillAbort.signal
3888
+ })
3889
+ },
3890
+ {
3891
+ name: "Ref",
3892
+ kind: REF_BACKFILL_JOB,
3893
+ isPending: hasPendingRefBackfill,
3894
+ createRunner: () => new RefBackfillRunner({
3895
+ dbPath,
3896
+ signal: backfillAbort.signal
3897
+ })
3898
+ }
3899
+ ], backfillAbort.signal);
3777
3900
  const syncRuntimeStatus = {
3778
3901
  phase: syncEnabled ? "starting" : "disabled",
3779
3902
  detail: syncEnabled ? "Waiting for viewer startup to finish" : "Sync is disabled"
@@ -3785,7 +3908,7 @@ async function startForegroundViewer(invocation) {
3785
3908
  getSyncRuntimeStatus: () => syncRuntimeStatus
3786
3909
  };
3787
3910
  const app = createApp(appOpts);
3788
- const pidPath = pidFilePath(resolveDbPath(invocation.dbPath ?? void 0));
3911
+ const pidPath = pidFilePath(dbPath);
3789
3912
  let syncServer = null;
3790
3913
  let syncListenerReady = false;
3791
3914
  if (syncEnabled) {
@@ -3820,18 +3943,7 @@ async function startForegroundViewer(invocation) {
3820
3943
  vectorMigrationRunner.start();
3821
3944
  p.log.step("Vector maintenance runner started");
3822
3945
  }
3823
- if (hasPendingDedupKeyBackfill(store.db)) {
3824
- dedupKeyBackfillRunner.start();
3825
- p.log.step("Dedup-key maintenance runner started");
3826
- }
3827
- if (hasPendingSessionContextBackfill(store.db)) {
3828
- sessionContextBackfillRunner.start();
3829
- p.log.step("Session-context backfill runner started");
3830
- }
3831
- if (hasPendingRefBackfill(store.db)) {
3832
- refBackfillRunner.start();
3833
- p.log.step("Ref backfill runner started");
3834
- }
3946
+ backfillCoordinator.start();
3835
3947
  if (syncConfig.syncRetentionEnabled) {
3836
3948
  retentionRunner.start();
3837
3949
  p.log.step("Retention maintenance runner started");
@@ -3889,12 +4001,11 @@ async function startForegroundViewer(invocation) {
3889
4001
  clearInterval(walCheckpointTimer);
3890
4002
  syncAbort.abort();
3891
4003
  retentionAbort.abort();
4004
+ backfillAbort.abort();
3892
4005
  await sweeper.stop();
3893
- await sessionContextBackfillRunner.stop();
3894
- await refBackfillRunner.stop();
3895
- await dedupKeyBackfillRunner.stop();
3896
4006
  await vectorMigrationRunner.stop();
3897
4007
  await retentionRunner.stop();
4008
+ await backfillCoordinator.stop();
3898
4009
  await new Promise((resolve) => {
3899
4010
  let remaining = syncServer ? 2 : 1;
3900
4011
  const done = () => {
@@ -3929,6 +4040,17 @@ async function startForegroundViewer(invocation) {
3929
4040
  }
3930
4041
  async function runServeInvocation(invocation) {
3931
4042
  const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
4043
+ const runtimeConflict = await findRuntimeViewerConflict(dbPath, {
4044
+ host: invocation.host,
4045
+ port: invocation.port
4046
+ });
4047
+ if (runtimeConflict) {
4048
+ p.intro("codemem viewer");
4049
+ p.log.error(`Database runtime at ${dirname(dbPath)} is already managed by viewer ${runtimeConflict.host}:${runtimeConflict.port} (pid ${runtimeConflict.pid})`);
4050
+ p.log.info("Use the matching --host/--port, stop the existing viewer first, or use a separate db/runtime folder for another viewer.");
4051
+ process.exitCode = 1;
4052
+ return;
4053
+ }
3932
4054
  if (invocation.mode === "stop" || invocation.mode === "restart") {
3933
4055
  const result = await stopExistingViewer(dbPath, {
3934
4056
  host: invocation.host,