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 +153 -30
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
|
3867
|
+
dbPath,
|
|
3791
3868
|
signal: retentionAbort.signal
|
|
3792
3869
|
});
|
|
3793
|
-
const vectorMigrationRunner = new VectorModelMigrationRunner({ dbPath
|
|
3794
|
-
const
|
|
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(
|
|
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
|
-
|
|
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();
|