feat: release v0.3.4 — offload local LLM, CLI restore, bugfix scripts

This commit is contained in:
chrishuan
2026-05-13 14:56:56 +08:00
parent 28be408fb8
commit d377b09fbc
63 changed files with 2191 additions and 14410 deletions
+14 -2
View File
@@ -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)}`);
+2 -2
View File
@@ -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)`);
}
}
}
+10
View File
@@ -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 [];
}
+1 -1
View File
@@ -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
}
+13 -3
View File
@@ -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`);
}