codemem 0.25.3 → 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, MemoryStore, ObserverClient, RawEventSweeper, 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, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, 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";
@@ -536,18 +536,6 @@ var MAX_FILES_MODIFIED = 64;
536
536
  var MAX_WORKING_SET_PATHS = 8;
537
537
  var MAX_QUERY_CHARS = 500;
538
538
  var MAX_QUERY_FILE_BASENAMES = 5;
539
- var MUTATING_TOOL_NAMES = new Set([
540
- "edit",
541
- "write",
542
- "multiedit",
543
- "notebookedit",
544
- "apply_patch"
545
- ]);
546
- var APPLY_PATCH_PATH_PREFIXES = [
547
- "*** Add File: ",
548
- "*** Update File: ",
549
- "*** Delete File: "
550
- ];
551
539
  function defaultSessionState() {
552
540
  return {
553
541
  first_prompt: "",
@@ -629,14 +617,6 @@ function clearSessionState(sessionId) {
629
617
  rmSync(path, { force: true });
630
618
  } catch {}
631
619
  }
632
- function extractApplyPatchPaths(patchText) {
633
- const out = [];
634
- for (const line of patchText.split("\n")) for (const prefix of APPLY_PATCH_PATH_PREFIXES) if (line.startsWith(prefix)) {
635
- const path = line.slice(prefix.length).trim();
636
- if (path) out.push(path);
637
- }
638
- return out;
639
- }
640
620
  function extractModifiedPathsFromHook(payload) {
641
621
  const toolName = String(payload.tool_name ?? "").trim().toLowerCase();
642
622
  if (!MUTATING_TOOL_NAMES.has(toolName)) return [];
@@ -3584,6 +3564,19 @@ function readViewerPidRecord(dbPath) {
3584
3564
  }
3585
3565
  return null;
3586
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
+ }
3587
3580
  function isProcessRunning(pid) {
3588
3581
  try {
3589
3582
  process.kill(pid, 0);
@@ -3755,6 +3748,88 @@ async function startBackgroundViewer(invocation) {
3755
3748
  p.intro("codemem viewer");
3756
3749
  p.outro(`Viewer started in background (pid ${child.pid}) at http://${invocation.host}:${invocation.port}`);
3757
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
+ }
3758
3833
  async function startForegroundViewer(invocation) {
3759
3834
  const { createApp, createSyncApp, closeStore, getStore } = await import("@codemem/server");
3760
3835
  const { serve } = await import("@hono/node-server");
@@ -3784,14 +3859,44 @@ async function startForegroundViewer(invocation) {
3784
3859
  sweeper.start();
3785
3860
  const syncAbort = new AbortController();
3786
3861
  const retentionAbort = new AbortController();
3862
+ const backfillAbort = new AbortController();
3787
3863
  const syncConfig = readCoordinatorSyncConfig(invocation.configPath ? readCodememConfigFileAtPath(invocation.configPath) : readCodememConfigFile());
3788
3864
  const syncEnabled = syncConfig.syncEnabled;
3865
+ const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
3789
3866
  const retentionRunner = new SyncRetentionRunner({
3790
- dbPath: resolveDbPath(invocation.dbPath ?? void 0),
3867
+ dbPath,
3791
3868
  signal: retentionAbort.signal
3792
3869
  });
3793
- const vectorMigrationRunner = new VectorModelMigrationRunner({ dbPath: resolveDbPath(invocation.dbPath ?? void 0) });
3794
- const dedupKeyBackfillRunner = new DedupKeyBackfillRunner({ 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);
3795
3900
  const syncRuntimeStatus = {
3796
3901
  phase: syncEnabled ? "starting" : "disabled",
3797
3902
  detail: syncEnabled ? "Waiting for viewer startup to finish" : "Sync is disabled"
@@ -3803,7 +3908,7 @@ async function startForegroundViewer(invocation) {
3803
3908
  getSyncRuntimeStatus: () => syncRuntimeStatus
3804
3909
  };
3805
3910
  const app = createApp(appOpts);
3806
- const pidPath = pidFilePath(resolveDbPath(invocation.dbPath ?? void 0));
3911
+ const pidPath = pidFilePath(dbPath);
3807
3912
  let syncServer = null;
3808
3913
  let syncListenerReady = false;
3809
3914
  if (syncEnabled) {
@@ -3838,10 +3943,7 @@ async function startForegroundViewer(invocation) {
3838
3943
  vectorMigrationRunner.start();
3839
3944
  p.log.step("Vector maintenance runner started");
3840
3945
  }
3841
- if (hasPendingDedupKeyBackfill(store.db)) {
3842
- dedupKeyBackfillRunner.start();
3843
- p.log.step("Dedup-key maintenance runner started");
3844
- }
3946
+ backfillCoordinator.start();
3845
3947
  if (syncConfig.syncRetentionEnabled) {
3846
3948
  retentionRunner.start();
3847
3949
  p.log.step("Retention maintenance runner started");
@@ -3899,10 +4001,11 @@ async function startForegroundViewer(invocation) {
3899
4001
  clearInterval(walCheckpointTimer);
3900
4002
  syncAbort.abort();
3901
4003
  retentionAbort.abort();
4004
+ backfillAbort.abort();
3902
4005
  await sweeper.stop();
3903
- await dedupKeyBackfillRunner.stop();
3904
4006
  await vectorMigrationRunner.stop();
3905
4007
  await retentionRunner.stop();
4008
+ await backfillCoordinator.stop();
3906
4009
  await new Promise((resolve) => {
3907
4010
  let remaining = syncServer ? 2 : 1;
3908
4011
  const done = () => {
@@ -3937,6 +4040,17 @@ async function startForegroundViewer(invocation) {
3937
4040
  }
3938
4041
  async function runServeInvocation(invocation) {
3939
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
+ }
3940
4054
  if (invocation.mode === "stop" || invocation.mode === "restart") {
3941
4055
  const result = await stopExistingViewer(dbPath, {
3942
4056
  host: invocation.host,
@@ -4822,12 +4936,14 @@ statusCmd.action((opts) => {
4822
4936
  last_sync_at: schema.syncPeers.last_sync_at,
4823
4937
  last_error: schema.syncPeers.last_error
4824
4938
  }).from(schema.syncPeers).all();
4939
+ const semanticIndex = getSemanticIndexDiagnostics(store.db);
4825
4940
  if (opts.json) {
4826
4941
  console.log(JSON.stringify({
4827
4942
  enabled: config.sync_enabled === true,
4828
4943
  host: config.sync_host ?? "0.0.0.0",
4829
4944
  port: config.sync_port ?? 7337,
4830
4945
  interval_s: config.sync_interval_s ?? 120,
4946
+ semantic_index: semanticIndex,
4831
4947
  device_id: deviceRow?.device_id ?? null,
4832
4948
  fingerprint: deviceRow?.fingerprint ?? null,
4833
4949
  coordinator_url: config.sync_coordinator_url ?? null,
@@ -4855,6 +4971,13 @@ statusCmd.action((opts) => {
4855
4971
  const label = peer.name || peer.peer_device_id;
4856
4972
  p.log.message(` ${label}: last_sync=${peer.last_sync_at ?? "never"}, status=${peer.last_error ?? "ok"}`);
4857
4973
  }
4974
+ p.log.info([
4975
+ "Semantic index:",
4976
+ ` State: ${semanticIndex.state}`,
4977
+ ` Summary: ${semanticIndex.summary}`,
4978
+ ` Coverage: ${semanticIndex.indexed_memory_count}/${semanticIndex.embeddable_memory_count}`,
4979
+ ` Mode: ${semanticIndex.mode === "semantic" ? `semantic (${semanticIndex.semantic_search_model ?? semanticIndex.current_model})` : "keyword-only"}`
4980
+ ].join("\n"));
4858
4981
  p.outro(`${peers.length} peer(s)`);
4859
4982
  } finally {
4860
4983
  store.close();