mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-10 20:34:30 +00:00
feat: release v0.3.4 — offload local LLM, CLI restore, bugfix scripts
This commit is contained in:
@@ -356,7 +356,7 @@ async function searchMemories(
|
||||
|
||||
// Resolve per-call embedding timeout for recall path.
|
||||
// Falls back to global embedding.timeoutMs when recallTimeoutMs is not configured.
|
||||
const recallEmbeddingTimeoutMs = cfg.embedding.recallTimeoutMs ?? cfg.embedding.timeoutMs;
|
||||
const recallEmbeddingTimeoutMs = cfg.embedding?.recallTimeoutMs ?? cfg.embedding?.timeoutMs;
|
||||
const embeddingCallOpts: EmbeddingCallOptions = { timeoutMs: recallEmbeddingTimeoutMs };
|
||||
|
||||
try {
|
||||
@@ -372,7 +372,19 @@ async function searchMemories(
|
||||
return { lines, timing: { ftsMs: 0, embeddingMs: performance.now() - tEmb, ftsHits: 0, embeddingHits: lines.length } };
|
||||
}
|
||||
|
||||
// Hybrid: run both keyword and embedding, merge with RRF
|
||||
// Hybrid: if the store natively supports hybrid search (e.g. TCVDB does
|
||||
// server-side dense + sparse + RRF in a single API call), short-circuit
|
||||
// to avoid a redundant second HTTP request and a wasted local embed().
|
||||
if (vectorStore?.getCapabilities().nativeHybridSearch) {
|
||||
const tNative = performance.now();
|
||||
const results = await vectorStore.searchL1Hybrid({ query: cleanText, topK: maxResults });
|
||||
const nativeMs = performance.now() - tNative;
|
||||
logger?.debug?.(`${TAG} [hybrid-native] Single-call hybrid: ${results.length} results in ${nativeMs.toFixed(0)}ms`);
|
||||
const lines = results.map((r) => formatMemoryLine(vectorResultToFormatable(r)));
|
||||
return { lines, timing: { ftsMs: 0, embeddingMs: nativeMs, ftsHits: 0, embeddingHits: results.length } };
|
||||
}
|
||||
|
||||
// Fallback: run keyword + embedding in parallel, merge with client-side RRF (SQLite path)
|
||||
return await searchHybrid(cleanText, pluginDataDir, maxResults, threshold, vectorStore!, embeddingService!, logger, embeddingCallOpts);
|
||||
} catch (err) {
|
||||
logger?.warn?.(`${TAG} Memory search failed (strategy=${effectiveStrategy}): ${err instanceof Error ? err.message : String(err)}`);
|
||||
|
||||
@@ -142,7 +142,7 @@ export async function pullProfilesToLocal(
|
||||
await fs.writeFile(target, record.content, "utf-8");
|
||||
if (md5(record.content) !== record.contentMd5) {
|
||||
await fs.rm(target, { force: true });
|
||||
logger.warn(`[memory-tdai][profile-sync] MD5 mismatch for ${record.filename}`);
|
||||
logger.debug?.(`[memory-tdai][profile-sync] MD5 mismatch for ${record.filename} (will re-pull on next sync)`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export async function pullProfilesToLocal(
|
||||
await fs.writeFile(path.join(tempDir, "persona.md"), body, "utf-8");
|
||||
if (md5(body) !== record.contentMd5) {
|
||||
await fs.rm(path.join(tempDir, "persona.md"), { force: true });
|
||||
logger.warn(`[memory-tdai][profile-sync] MD5 mismatch for ${record.filename}`);
|
||||
logger.debug?.(`[memory-tdai][profile-sync] MD5 mismatch for ${record.filename} (will re-pull on next sync)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +318,11 @@ async function callLlmExtraction(params: {
|
||||
previousSceneName,
|
||||
});
|
||||
|
||||
// [l1-debug] ENTRY — what are we about to ask the LLM to extract?
|
||||
logger?.debug?.(
|
||||
`${TAG} [l1-debug] ENTRY taskId=l1-extraction, newMsgs=${newMessages.length}, bgMsgs=${backgroundMessages.length}, userPromptLen=${userPrompt.length}, sysPromptLen=${EXTRACT_MEMORIES_SYSTEM_PROMPT.length}, model=${model ?? "(default)"}, previousSceneName=${previousSceneName ? JSON.stringify(previousSceneName) : "(none)"}, runnerKind=${llmRunner ? "llmRunner" : "CleanContextRunner"}`,
|
||||
);
|
||||
|
||||
let result: string;
|
||||
|
||||
if (llmRunner) {
|
||||
@@ -364,6 +369,11 @@ function parseExtractionResult(raw: string, logger?: Logger): SceneSegment[] {
|
||||
const arrayMatch = cleaned.match(/\[[\s\S]*\]/);
|
||||
if (!arrayMatch) {
|
||||
logger?.warn?.(`${TAG} No JSON array found in extraction response`);
|
||||
// [l1-debug] NO_JSON — dump the full raw so we can see what the LLM actually said
|
||||
const rawPreview = raw.slice(0, 2048);
|
||||
logger?.warn?.(
|
||||
`${TAG} [l1-debug] NO_JSON taskId=l1-extraction, rawLen=${raw.length}, cleanedLen=${cleaned.length}, rawFull=${JSON.stringify(rawPreview)}${raw.length > 2048 ? `…(+${raw.length - 2048})` : ""}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -665,7 +665,7 @@ export class VectorStore implements IMemoryStore {
|
||||
// Migration: add timestamp column if missing (existing DBs pre-v3.x)
|
||||
try {
|
||||
this.db.exec("ALTER TABLE l0_conversations ADD COLUMN timestamp INTEGER DEFAULT 0");
|
||||
this.logger?.info(`${TAG} Migrated l0_conversations: added timestamp column`);
|
||||
this.logger?.debug?.(`${TAG} Migrated l0_conversations: added timestamp column`);
|
||||
} catch {
|
||||
// Column already exists — expected on non-first run
|
||||
}
|
||||
|
||||
@@ -120,10 +120,12 @@ export class TcvdbClient {
|
||||
*/
|
||||
async request<T = ApiResponse>(path: string, body: Record<string, unknown>): Promise<T> {
|
||||
let lastError: Error | undefined;
|
||||
const t0 = performance.now();
|
||||
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
const tAttempt = performance.now();
|
||||
try {
|
||||
this.logger?.debug?.(`${TAG} → ${path} body=${JSON.stringify(body).slice(0, 500)}`);
|
||||
this.logger?.debug?.(`${TAG} → ${path} attempt=${attempt} body=${JSON.stringify(body).slice(0, 500)}`);
|
||||
const { statusCode, body: respBody } = await undiciRequest(`${this.baseUrl}${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -137,7 +139,8 @@ export class TcvdbClient {
|
||||
|
||||
const text = await respBody.text();
|
||||
const json = JSON.parse(text) as ApiResponse;
|
||||
this.logger?.debug?.(`${TAG} ← ${path} status=${statusCode} code=${json.code} msg=${json.msg} keys=[${Object.keys(json).join(",")}]`);
|
||||
const attemptMs = Math.round(performance.now() - tAttempt);
|
||||
this.logger?.debug?.(`${TAG} ← ${path} status=${statusCode} code=${json.code} attemptMs=${attemptMs} attempt=${attempt}`);
|
||||
|
||||
if (json.code !== 0) {
|
||||
const err = new TcvdbApiError(path, json.code, json.msg);
|
||||
@@ -146,18 +149,25 @@ export class TcvdbClient {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always log completion at info level (one line per request)
|
||||
const totalMs = Math.round(performance.now() - t0);
|
||||
this.logger?.info(`${TAG} ${path} ${totalMs}ms${attempt > 0 ? ` (${attempt + 1} attempts)` : ""}`);
|
||||
|
||||
return json as unknown as T;
|
||||
} catch (err) {
|
||||
const attemptMs = Math.round(performance.now() - tAttempt);
|
||||
if (err instanceof TcvdbApiError && err.apiCode !== 0) throw err;
|
||||
lastError = err instanceof Error ? err : new Error(String(err));
|
||||
if (attempt < MAX_RETRIES) {
|
||||
const delay = 500 * (attempt + 1);
|
||||
this.logger?.debug?.(`${TAG} ${path} retry ${attempt + 1}/${MAX_RETRIES} in ${delay}ms`);
|
||||
this.logger?.debug?.(`${TAG} ${path} retry ${attempt + 1}/${MAX_RETRIES} in ${delay}ms (lastAttemptMs=${attemptMs}, error=${lastError.message})`);
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalMs = Math.round(performance.now() - t0);
|
||||
this.logger?.debug?.(`${TAG} ✗ ${path} totalMs=${totalMs} attempts=${MAX_RETRIES + 1} error=${lastError?.message}`);
|
||||
throw lastError ?? new Error(`${TAG} ${path} failed after retries`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user