Files
memind/memind-zvec-workspace.mjs
T
john 40e4a527fe feat(context): add recall fusion and zvec workspace adapters
Introduce RRF-based recall fusion for personal/episodic/temporal paths,
zvec-grep workspace shadow probing for code executors, and a local
headroom proxy starter aligned with the fusion plan defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 21:28:04 +08:00

124 lines
3.5 KiB
JavaScript

import { spawnSync } from 'node:child_process';
import path from 'node:path';
export const DEFAULT_ZVEC_QUERY_LIMIT = 8;
function truthy(value, fallback = false) {
const raw = String(value ?? '').trim().toLowerCase();
if (!raw) return fallback;
return ['1', 'true', 'yes', 'on'].includes(raw);
}
export function resolveZvecWorkspaceMode(env = process.env) {
const raw = String(env.MEMIND_ZVEC_WORKSPACE_MODE ?? 'off').trim().toLowerCase();
return ['off', 'shadow', 'active'].includes(raw) ? raw : 'off';
}
export function resolveZvecBinary(env = process.env) {
return String(env.MEMIND_ZVEC_BINARY ?? 'zg').trim() || 'zg';
}
export function resolveZvecWorkspaceRoot(cwd, env = process.env) {
const explicit = String(env.MEMIND_ZVEC_WORKSPACE_ROOT ?? '').trim();
if (explicit) return path.resolve(explicit);
return path.resolve(String(cwd ?? process.cwd()));
}
function parseZvecQueryOutput(stdout) {
const hits = [];
const lines = String(stdout ?? '').split('\n');
for (const line of lines) {
const match = line.match(/^#(\d+) matchedBy=([^\s]+)\s+(.+?):(\d+)-(\d+)/);
if (!match) continue;
hits.push({
rank: Number(match[1]),
matchedBy: match[2],
path: match[3].trim(),
startLine: Number(match[4]),
endLine: Number(match[5]),
});
}
return hits;
}
export function searchZvecWorkspace({
query,
limit = DEFAULT_ZVEC_QUERY_LIMIT,
cwd,
env = process.env,
spawnImpl = spawnSync,
} = {}) {
const mode = resolveZvecWorkspaceMode(env);
const text = String(query ?? '').trim();
const workspaceRoot = resolveZvecWorkspaceRoot(cwd, env);
if (mode === 'off' || !text) {
return {
mode,
query: text,
hits: [],
applied: false,
skipped: true,
reason: mode === 'off' ? 'disabled' : 'empty_query',
};
}
const binary = resolveZvecBinary(env);
const result = spawnImpl(binary, ['query', text, '--limit', String(limit)], {
cwd: workspaceRoot,
env: { ...process.env, ...env },
encoding: 'utf8',
});
if (result.status !== 0) {
return {
mode,
query: text,
hits: [],
applied: false,
skipped: true,
reason: 'zvec_query_failed',
stderr: String(result.stderr ?? '').trim().slice(0, 400),
};
}
const hits = parseZvecQueryOutput(result.stdout);
return {
mode,
query: text,
workspaceRoot,
hits,
hitCount: hits.length,
applied: mode === 'active',
skipped: false,
stdoutPreview: String(result.stdout ?? '').trim().slice(0, 1200),
};
}
export function formatZvecWorkspaceBlock(result) {
if (!result?.hits?.length) return '';
const lines = [
'【Workspace 代码检索】',
'以下片段来自本工作区索引,仅供开发/排查参考,不是用户生活记忆。',
];
for (const hit of result.hits.slice(0, 8)) {
lines.push(
`- ${hit.path}:${hit.startLine}-${hit.endLine} (${hit.matchedBy})`,
);
}
return lines.join('\n').trim();
}
export function isZvecWorkspaceEnabled(env = process.env) {
return resolveZvecWorkspaceMode(env) !== 'off';
}
export function shouldAttachZvecWorkspaceForTask(taskType, env = process.env) {
if (!isZvecWorkspaceEnabled(env)) return false;
const raw = String(env.MEMIND_ZVEC_WORKSPACE_TASK_TYPES ?? 'page_data_dev_complex,repo_refactor,multi_file')
.split(',')
.map((item) => item.trim().toLowerCase())
.filter(Boolean);
const normalizedTask = String(taskType ?? '').trim().toLowerCase();
return raw.includes(normalizedTask);
}