add update

This commit is contained in:
Hande 2026-01-29 20:48:23 +01:00
parent 79a157db62
commit c71eca5237
5 changed files with 246 additions and 20 deletions

View File

@ -11,8 +11,6 @@
# - Vector DB: LanceDB (file-based)
# - Graph DB: Kuzu (file-based)
version: "3.8"
services:
cognee:
image: cognee/cognee:latest
@ -21,6 +19,7 @@ services:
- "127.0.0.1:8000:8000"
environment:
- LLM_API_KEY=${LLM_API_KEY}
- ENABLE_BACKEND_ACCESS_CONTROL=false
volumes:
- cognee_data:/app/cognee/.cognee_system
restart: unless-stopped

View File

@ -24,6 +24,7 @@ type MemoryCommandOptions = {
json?: boolean;
deep?: boolean;
index?: boolean;
updateCognee?: boolean;
verbose?: boolean;
};
@ -197,6 +198,7 @@ async function scanMemorySources(params: {
export async function runMemoryStatus(opts: MemoryCommandOptions) {
setVerbose(Boolean(opts.verbose));
const updateCognee = opts.updateCognee ?? process.argv.includes("--update-cognee");
const cfg = loadConfig();
const agentIds = resolveAgentIds(cfg, opts.agent);
const allResults: Array<{
@ -205,6 +207,7 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
embeddingProbe?: Awaited<ReturnType<MemoryManager["probeEmbeddingAvailability"]>>;
indexError?: string;
scan?: MemorySourceScan;
cogneeUpdate?: boolean;
}> = [];
for (const agentId of agentIds) {
@ -240,6 +243,7 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
try {
await manager.sync({
reason: "cli",
update: updateCognee,
progress: (syncUpdate) => {
update({
completed: syncUpdate.completed,
@ -269,7 +273,14 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
agentId,
sources,
});
allResults.push({ agentId, status, embeddingProbe, indexError, scan });
allResults.push({
agentId,
status,
embeddingProbe,
indexError,
scan,
cogneeUpdate: Boolean(updateCognee),
});
},
});
}
@ -434,6 +445,7 @@ export function registerMemoryCli(program: Command) {
.option("--json", "Print JSON")
.option("--deep", "Probe embedding provider availability")
.option("--index", "Reindex if dirty (implies --deep)")
.option("--update-cognee", "Use Cognee update when file data ids are known", false)
.option("--verbose", "Verbose logging", false)
.action(async (opts: MemoryCommandOptions) => {
await runMemoryStatus(opts);

View File

@ -24,6 +24,21 @@ export type CogneeAddResponse = {
datasetId: string;
datasetName: string;
message: string;
dataId?: string;
};
export type CogneeUpdateRequest = {
dataId: string;
datasetId: string;
data: string;
};
export type CogneeUpdateResponse = {
datasetId?: string;
datasetName?: string;
message?: string;
status?: string;
dataId?: string;
};
export type CogneeCognifyRequest = {
@ -119,12 +134,15 @@ export class CogneeClient {
dataset_id: string;
dataset_name: string;
message: string;
data_id?: unknown;
data_ingestion_info?: unknown;
};
return {
datasetId: data.dataset_id,
datasetName: data.dataset_name,
message: data.message,
dataId: this.extractDataId(data.data_id ?? data.data_ingestion_info),
};
} catch (error) {
log.error("Failed to add data to Cognee", { error });
@ -134,6 +152,63 @@ export class CogneeClient {
}
}
async update(req: CogneeUpdateRequest): Promise<CogneeUpdateResponse> {
const url = new URL(`${this.baseUrl}${API_PREFIX}/update`);
url.searchParams.set("data_id", req.dataId);
url.searchParams.set("dataset_id", req.datasetId);
const headers: Record<string, string> = {};
if (this.apiKey) {
headers.Authorization = `Bearer ${this.apiKey}`;
headers["X-Api-Key"] = this.apiKey;
}
log.debug("Updating data in Cognee", {
url: url.toString(),
dataLength: req.data.length,
});
try {
const formData = new FormData();
const blob = new Blob([req.data], { type: "text/plain" });
formData.append("data", blob, "clawdbot-memory.txt");
const response = await request(url.toString(), {
method: "PATCH",
headers,
body: formData,
bodyTimeout: this.timeoutMs,
headersTimeout: this.timeoutMs,
});
if (response.statusCode !== 200) {
const errorText = await response.body.text();
throw new Error(`Cognee update failed with status ${response.statusCode}: ${errorText}`);
}
const data = (await response.body.json()) as {
status?: string;
message?: string;
dataset_id?: string;
dataset_name?: string;
data_id?: unknown;
data_ingestion_info?: unknown;
};
return {
status: data.status,
message: data.message,
datasetId: data.dataset_id ?? req.datasetId,
datasetName: data.dataset_name,
dataId: this.extractDataId(data.data_id ?? data.data_ingestion_info) ?? req.dataId,
};
} catch (error) {
log.error("Failed to update data in Cognee", { error });
throw new Error(
`Cognee update request failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async cognify(req: CogneeCognifyRequest = {}): Promise<CogneeCognifyResponse> {
const url = `${this.baseUrl}${API_PREFIX}/cognify`;
const headers: Record<string, string> = {
@ -265,6 +340,22 @@ export class CogneeClient {
}
}
private extractDataId(value: unknown): string | undefined {
if (!value) return undefined;
if (typeof value === "string") return value;
if (Array.isArray(value)) {
for (const entry of value) {
const id = this.extractDataId(entry);
if (id) return id;
}
return undefined;
}
if (typeof value !== "object") return undefined;
const record = value as { data_id?: unknown; data_ingestion_info?: unknown };
if (typeof record.data_id === "string") return record.data_id;
return this.extractDataId(record.data_ingestion_info);
}
async healthCheck(): Promise<boolean> {
try {
await this.status();

View File

@ -1,7 +1,9 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { MoltbotConfig } from "../config/config.js";
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
import { resolveStateDir } from "../config/paths.js";
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import type { MemorySearchResult } from "./index.js";
@ -25,6 +27,12 @@ const DEFAULT_AUTO_COGNIFY = true;
const DEFAULT_COGNIFY_BATCH_SIZE = 100;
const SNIPPET_MAX_CHARS = 700;
type CogneeSyncIndex = {
datasetId?: string;
datasetName?: string;
files: Record<string, { hash: string; dataId?: string }>;
};
export type CogneeProviderConfig = {
baseUrl?: string;
apiKey?: string;
@ -51,6 +59,10 @@ export class CogneeMemoryProvider {
private readonly sources: Set<CogneeMemorySource>;
private datasetId?: string;
private syncedFiles = new Map<string, string>(); // path -> hash
private readonly syncIndexPath: string;
private syncIndexLoaded = false;
private syncIndex: CogneeSyncIndex = { files: {} };
private syncIndexDirty = false;
constructor(
cfg: MoltbotConfig,
@ -75,6 +87,12 @@ export class CogneeMemoryProvider {
this.autoCognify = config.autoCognify ?? DEFAULT_AUTO_COGNIFY;
this.cognifyBatchSize = config.cognifyBatchSize || DEFAULT_COGNIFY_BATCH_SIZE;
this.sources = new Set(sources);
this.syncIndexPath = path.join(
resolveStateDir(process.env, os.homedir),
"memory",
"cognee",
`${agentId}.json`,
);
log.info("Cognee memory provider initialized", {
agentId,
@ -91,30 +109,38 @@ export class CogneeMemoryProvider {
async sync(params?: {
reason?: string;
force?: boolean;
update?: boolean;
progress?: (update: { completed: number; total: number; label?: string }) => void;
}): Promise<void> {
log.info("Starting Cognee memory sync", { agentId: this.agentId });
let addedCount = 0;
await this.loadSyncIndex();
const force = Boolean(params?.force);
const update = Boolean(params?.update);
// Sync memory files
if (this.sources.has("memory")) {
const memoryFiles = await this.collectMemoryFiles();
addedCount += await this.syncFiles(memoryFiles, "memory");
addedCount += await this.syncFiles(memoryFiles, "memory", { update });
}
// Sync session transcripts
if (this.sources.has("sessions")) {
const sessionFiles = await this.collectSessionFiles();
addedCount += await this.syncFiles(sessionFiles, "sessions");
addedCount += await this.syncFiles(sessionFiles, "sessions", { update });
}
// Run cognify if auto-enabled and files were added
if (this.autoCognify && addedCount > 0) {
if ((this.autoCognify && addedCount > 0) || (this.autoCognify && force)) {
log.info("Running cognify after sync", { addedCount });
await this.cognify();
}
if (this.syncIndexDirty) {
await this.saveSyncIndex();
}
log.info("Cognee memory sync completed", {
agentId: this.agentId,
addedCount,
@ -288,6 +314,60 @@ export class CogneeMemoryProvider {
async close(): Promise<void> {}
private async loadSyncIndex(): Promise<void> {
if (this.syncIndexLoaded) return;
this.syncIndexLoaded = true;
try {
const raw = await fs.readFile(this.syncIndexPath, "utf-8");
const parsed = JSON.parse(raw) as CogneeSyncIndex;
if (!parsed || typeof parsed !== "object") return;
this.syncIndex = {
datasetId: parsed.datasetId,
datasetName: parsed.datasetName,
files: parsed.files && typeof parsed.files === "object" ? parsed.files : {},
};
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code !== "ENOENT") {
log.warn("Failed to load Cognee sync index", { error });
}
}
if (this.syncIndex.datasetName && this.syncIndex.datasetName !== this.datasetName) {
log.info("Resetting Cognee sync index (dataset name changed)", {
from: this.syncIndex.datasetName,
to: this.datasetName,
});
this.syncIndex = { files: {} };
this.syncIndexDirty = true;
}
if (this.syncIndex.datasetId && this.datasetId && this.syncIndex.datasetId !== this.datasetId) {
log.info("Resetting Cognee sync index (dataset id changed)", {
from: this.syncIndex.datasetId,
to: this.datasetId,
});
this.syncIndex = { files: {} };
this.syncIndexDirty = true;
}
if (!this.datasetId && this.syncIndex.datasetId) {
this.datasetId = this.syncIndex.datasetId;
}
}
private async saveSyncIndex(): Promise<void> {
const dir = path.dirname(this.syncIndexPath);
await fs.mkdir(dir, { recursive: true });
const payload: CogneeSyncIndex = {
datasetId: this.datasetId ?? this.syncIndex.datasetId,
datasetName: this.datasetName,
files: this.syncIndex.files,
};
await fs.writeFile(this.syncIndexPath, JSON.stringify(payload, null, 2), "utf-8");
this.syncIndexDirty = false;
}
private async collectMemoryFiles(): Promise<MemoryFileEntry[]> {
const files: MemoryFileEntry[] = [];
const memoryPaths = await listMemoryFiles(this.workspaceDir);
@ -337,9 +417,14 @@ export class CogneeMemoryProvider {
return files;
}
private async syncFiles(files: MemoryFileEntry[], source: CogneeMemorySource): Promise<number> {
private async syncFiles(
files: MemoryFileEntry[],
source: CogneeMemorySource,
opts?: { update?: boolean },
): Promise<number> {
let addedCount = 0;
const batchSize = this.cognifyBatchSize;
const update = Boolean(opts?.update);
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
@ -363,24 +448,62 @@ export class CogneeMemoryProvider {
const dataWithMetadata = `# ${file.path}\n\n${content}\n\n---\nMetadata: ${JSON.stringify(metadata)}`;
const response = await this.client.add({
data: dataWithMetadata,
datasetName: this.datasetName,
});
const record = this.syncIndex.files[file.path];
const datasetId = this.datasetId ?? this.syncIndex.datasetId;
const canUpdate = update && record?.dataId && datasetId;
if (!this.datasetId) {
this.datasetId = response.datasetId;
if (canUpdate && datasetId && record?.dataId) {
await this.client.update({
dataId: record.dataId,
datasetId,
data: dataWithMetadata,
});
addedCount++;
log.debug("Updated file in Cognee", {
path: file.path,
datasetId,
dataId: record.dataId,
});
} else {
const response = await this.client.add({
data: dataWithMetadata,
datasetName: this.datasetName,
datasetId,
});
if (!this.datasetId) {
this.datasetId = response.datasetId;
}
if (response.dataId) {
this.syncIndex.files[file.path] = {
hash: file.hash,
dataId: response.dataId,
};
} else {
this.syncIndex.files[file.path] = { hash: file.hash };
}
this.syncIndex.datasetId = this.datasetId ?? this.syncIndex.datasetId;
this.syncIndex.datasetName = this.datasetName;
this.syncIndexDirty = true;
this.syncedFiles.set(file.path, file.hash);
addedCount++;
log.debug("Added file to Cognee", {
path: file.path,
datasetId: response.datasetId,
});
continue;
}
const dataId = record?.dataId;
this.syncIndex.files[file.path] = { hash: file.hash, dataId };
this.syncIndex.datasetId = datasetId ?? this.syncIndex.datasetId;
this.syncIndex.datasetName = this.datasetName;
this.syncIndexDirty = true;
this.syncedFiles.set(file.path, file.hash);
addedCount++;
log.debug("Added file to Cognee", {
path: file.path,
datasetId: response.datasetId,
});
} catch (error) {
log.error("Failed to add file to Cognee", { path: file.path, error });
log.error("Failed to sync file to Cognee", { path: file.path, error });
}
}
}

View File

@ -384,6 +384,7 @@ export class MemoryIndexManager {
async sync(params?: {
reason?: string;
force?: boolean;
update?: boolean;
progress?: (update: MemorySyncProgressUpdate) => void;
}): Promise<void> {
if (this.syncing) return this.syncing;