mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
Lint: add braces for single-line ifs
This commit is contained in:
committed by
Vignesh
parent
23cfcd60df
commit
e332a717a8
@@ -104,6 +104,7 @@ BM25 + vectors + reranking. Markdown stays the source of truth; Moltbot shells
|
||||
out to QMD for retrieval. Key points:
|
||||
|
||||
**Prereqs**
|
||||
|
||||
- Disabled by default. Opt in per-config (`memory.backend = "qmd"`).
|
||||
- Install the QMD CLI separately (`bun install -g github.com/tobi/qmd` or grab
|
||||
a release) and make sure the `qmd` binary is on the gateway’s `PATH`.
|
||||
@@ -118,6 +119,7 @@ out to QMD for retrieval. Key points:
|
||||
installed. Windows is best supported via WSL2.
|
||||
|
||||
**How the sidecar runs**
|
||||
|
||||
- The gateway writes a self-contained QMD home under
|
||||
`~/.openclaw/agents/<agentId>/qmd/` (config + cache + sqlite DB).
|
||||
- Collections are rewritten from `memory.qmd.paths` (plus default workspace
|
||||
@@ -160,6 +162,7 @@ out to QMD for retrieval. Key points:
|
||||
```
|
||||
|
||||
**Config surface (`memory.qmd.*`)**
|
||||
|
||||
- `command` (default `qmd`): override the executable path.
|
||||
- `includeDefaultMemory` (default `true`): auto-index `MEMORY.md` + `memory/**/*.md`.
|
||||
- `paths[]`: add extra directories/files (`path`, optional `pattern`, optional
|
||||
@@ -207,6 +210,7 @@ memory: {
|
||||
```
|
||||
|
||||
**Citations & fallback**
|
||||
|
||||
- `memory.citations` applies regardless of backend (`auto`/`on`/`off`).
|
||||
- When `qmd` runs, we tag `status().backend = "qmd"` so diagnostics show which
|
||||
engine served the results. If the QMD subprocess exits or JSON output can’t be
|
||||
|
||||
@@ -48,7 +48,9 @@ describe("memory search citations", () => {
|
||||
backend = "builtin";
|
||||
const cfg = { memory: { citations: "on" }, agents: { list: [{ id: "main", default: true }] } };
|
||||
const tool = createMemorySearchTool({ config: cfg });
|
||||
if (!tool) throw new Error("tool missing");
|
||||
if (!tool) {
|
||||
throw new Error("tool missing");
|
||||
}
|
||||
const result = await tool.execute("call_citations_on", { query: "notes" });
|
||||
const details = result.details as { results: Array<{ snippet: string; citation?: string }> };
|
||||
expect(details.results[0]?.snippet).toMatch(/Source: MEMORY.md#L5-L7/);
|
||||
@@ -59,7 +61,9 @@ describe("memory search citations", () => {
|
||||
backend = "builtin";
|
||||
const cfg = { memory: { citations: "off" }, agents: { list: [{ id: "main", default: true }] } };
|
||||
const tool = createMemorySearchTool({ config: cfg });
|
||||
if (!tool) throw new Error("tool missing");
|
||||
if (!tool) {
|
||||
throw new Error("tool missing");
|
||||
}
|
||||
const result = await tool.execute("call_citations_off", { query: "notes" });
|
||||
const details = result.details as { results: Array<{ snippet: string; citation?: string }> };
|
||||
expect(details.results[0]?.snippet).not.toMatch(/Source:/);
|
||||
@@ -73,7 +77,9 @@ describe("memory search citations", () => {
|
||||
agents: { list: [{ id: "main", default: true }] },
|
||||
};
|
||||
const tool = createMemorySearchTool({ config: cfg });
|
||||
if (!tool) throw new Error("tool missing");
|
||||
if (!tool) {
|
||||
throw new Error("tool missing");
|
||||
}
|
||||
const result = await tool.execute("call_citations_qmd", { query: "notes" });
|
||||
const details = result.details as { results: Array<{ snippet: string; citation?: string }> };
|
||||
expect(details.results[0]?.snippet.length).toBeLessThanOrEqual(20);
|
||||
|
||||
@@ -128,7 +128,9 @@ export function createMemoryGetTool(options: {
|
||||
|
||||
function resolveMemoryCitationsMode(cfg: MoltbotConfig): MemoryCitationsMode {
|
||||
const mode = cfg.memory?.citations;
|
||||
if (mode === "on" || mode === "off" || mode === "auto") return mode;
|
||||
if (mode === "on" || mode === "off" || mode === "auto") {
|
||||
return mode;
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
@@ -155,11 +157,15 @@ function clampResultsByInjectedChars(
|
||||
results: MemorySearchResult[],
|
||||
budget?: number,
|
||||
): MemorySearchResult[] {
|
||||
if (!budget || budget <= 0) return results;
|
||||
if (!budget || budget <= 0) {
|
||||
return results;
|
||||
}
|
||||
let remaining = budget;
|
||||
const clamped: MemorySearchResult[] = [];
|
||||
for (const entry of results) {
|
||||
if (remaining <= 0) break;
|
||||
if (remaining <= 0) {
|
||||
break;
|
||||
}
|
||||
const snippet = entry.snippet ?? "";
|
||||
if (snippet.length <= remaining) {
|
||||
clamped.push(entry);
|
||||
@@ -177,16 +183,26 @@ function shouldIncludeCitations(params: {
|
||||
mode: MemoryCitationsMode;
|
||||
sessionKey?: string;
|
||||
}): boolean {
|
||||
if (params.mode === "on") return true;
|
||||
if (params.mode === "off") return false;
|
||||
if (params.mode === "on") {
|
||||
return true;
|
||||
}
|
||||
if (params.mode === "off") {
|
||||
return false;
|
||||
}
|
||||
// auto: show citations in direct chats; suppress in groups/channels by default.
|
||||
const chatType = deriveChatTypeFromSessionKey(params.sessionKey);
|
||||
return chatType === "direct";
|
||||
}
|
||||
|
||||
function deriveChatTypeFromSessionKey(sessionKey?: string): "direct" | "group" | "channel" {
|
||||
if (!sessionKey) return "direct";
|
||||
if (sessionKey.includes(":group:")) return "group";
|
||||
if (sessionKey.includes(":channel:")) return "channel";
|
||||
if (!sessionKey) {
|
||||
return "direct";
|
||||
}
|
||||
if (sessionKey.includes(":group:")) {
|
||||
return "group";
|
||||
}
|
||||
if (sessionKey.includes(":channel:")) {
|
||||
return "channel";
|
||||
}
|
||||
return "direct";
|
||||
}
|
||||
|
||||
@@ -100,7 +100,9 @@ function ensureUniqueName(base: string, existing: Set<string>): string {
|
||||
|
||||
function resolvePath(raw: string, workspaceDir: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) throw new Error("path required");
|
||||
if (!trimmed) {
|
||||
throw new Error("path required");
|
||||
}
|
||||
if (trimmed.startsWith("~") || path.isAbsolute(trimmed)) {
|
||||
return path.normalize(resolveUserPath(trimmed));
|
||||
}
|
||||
@@ -109,7 +111,9 @@ function resolvePath(raw: string, workspaceDir: string): string {
|
||||
|
||||
function resolveIntervalMs(raw: string | undefined): number {
|
||||
const value = raw?.trim();
|
||||
if (!value) return parseDurationMs(DEFAULT_QMD_INTERVAL, { defaultUnit: "m" });
|
||||
if (!value) {
|
||||
return parseDurationMs(DEFAULT_QMD_INTERVAL, { defaultUnit: "m" });
|
||||
}
|
||||
try {
|
||||
return parseDurationMs(value, { defaultUnit: "m" });
|
||||
} catch {
|
||||
@@ -119,7 +123,9 @@ function resolveIntervalMs(raw: string | undefined): number {
|
||||
|
||||
function resolveEmbedIntervalMs(raw: string | undefined): number {
|
||||
const value = raw?.trim();
|
||||
if (!value) return parseDurationMs(DEFAULT_QMD_EMBED_INTERVAL, { defaultUnit: "m" });
|
||||
if (!value) {
|
||||
return parseDurationMs(DEFAULT_QMD_EMBED_INTERVAL, { defaultUnit: "m" });
|
||||
}
|
||||
try {
|
||||
return parseDurationMs(value, { defaultUnit: "m" });
|
||||
} catch {
|
||||
@@ -136,7 +142,9 @@ function resolveDebounceMs(raw: number | undefined): number {
|
||||
|
||||
function resolveLimits(raw?: MemoryQmdConfig["limits"]): ResolvedQmdLimitsConfig {
|
||||
const parsed: ResolvedQmdLimitsConfig = { ...DEFAULT_QMD_LIMITS };
|
||||
if (raw?.maxResults && raw.maxResults > 0) parsed.maxResults = Math.floor(raw.maxResults);
|
||||
if (raw?.maxResults && raw.maxResults > 0) {
|
||||
parsed.maxResults = Math.floor(raw.maxResults);
|
||||
}
|
||||
if (raw?.maxSnippetChars && raw.maxSnippetChars > 0) {
|
||||
parsed.maxSnippetChars = Math.floor(raw.maxSnippetChars);
|
||||
}
|
||||
@@ -170,11 +178,15 @@ function resolveCustomPaths(
|
||||
workspaceDir: string,
|
||||
existing: Set<string>,
|
||||
): ResolvedQmdCollection[] {
|
||||
if (!rawPaths?.length) return [];
|
||||
if (!rawPaths?.length) {
|
||||
return [];
|
||||
}
|
||||
const collections: ResolvedQmdCollection[] = [];
|
||||
rawPaths.forEach((entry, index) => {
|
||||
const trimmedPath = entry?.path?.trim();
|
||||
if (!trimmedPath) return;
|
||||
if (!trimmedPath) {
|
||||
return;
|
||||
}
|
||||
let resolved: string;
|
||||
try {
|
||||
resolved = resolvePath(trimmedPath, workspaceDir);
|
||||
@@ -199,7 +211,9 @@ function resolveDefaultCollections(
|
||||
workspaceDir: string,
|
||||
existing: Set<string>,
|
||||
): ResolvedQmdCollection[] {
|
||||
if (!include) return [];
|
||||
if (!include) {
|
||||
return [];
|
||||
}
|
||||
const entries: Array<{ path: string; pattern: string; base: string }> = [
|
||||
{ path: workspaceDir, pattern: "MEMORY.md", base: "memory-root" },
|
||||
{ path: workspaceDir, pattern: "memory.md", base: "memory-alt" },
|
||||
|
||||
@@ -75,7 +75,9 @@ describe("QmdMemoryManager", () => {
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
||||
expect(manager).toBeTruthy();
|
||||
if (!manager) throw new Error("manager missing");
|
||||
if (!manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
|
||||
const baselineCalls = spawnMock.mock.calls.length;
|
||||
|
||||
@@ -114,7 +116,9 @@ describe("QmdMemoryManager", () => {
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
||||
expect(manager).toBeTruthy();
|
||||
if (!manager) throw new Error("manager missing");
|
||||
if (!manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
|
||||
const isAllowed = (key?: string) =>
|
||||
(manager as unknown as { isScopeAllowed: (key?: string) => boolean }).isScopeAllowed(key);
|
||||
@@ -128,7 +132,9 @@ describe("QmdMemoryManager", () => {
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
||||
expect(manager).toBeTruthy();
|
||||
if (!manager) throw new Error("manager missing");
|
||||
if (!manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
|
||||
const textPath = path.join(workspaceDir, "secret.txt");
|
||||
await fs.writeFile(textPath, "nope", "utf-8");
|
||||
|
||||
@@ -55,7 +55,9 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
resolved: ResolvedMemoryBackendConfig;
|
||||
}): Promise<QmdMemoryManager | null> {
|
||||
const resolved = params.resolved.qmd;
|
||||
if (!resolved) return null;
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
const manager = new QmdMemoryManager({ cfg: params.cfg, agentId: params.agentId, resolved });
|
||||
await manager.initialize();
|
||||
return manager;
|
||||
@@ -174,10 +176,13 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
const parsed = JSON.parse(result.stdout) as unknown;
|
||||
if (Array.isArray(parsed)) {
|
||||
for (const entry of parsed) {
|
||||
if (typeof entry === "string") existing.add(entry);
|
||||
else if (entry && typeof entry === "object") {
|
||||
if (typeof entry === "string") {
|
||||
existing.add(entry);
|
||||
} else if (entry && typeof entry === "object") {
|
||||
const name = (entry as { name?: unknown }).name;
|
||||
if (typeof name === "string") existing.add(name);
|
||||
if (typeof name === "string") {
|
||||
existing.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,7 +191,9 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
for (const collection of this.qmd.collections) {
|
||||
if (existing.has(collection.name)) continue;
|
||||
if (existing.has(collection.name)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await this.runQmd([
|
||||
"collection",
|
||||
@@ -200,8 +207,12 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
// Idempotency: qmd exits non-zero if the collection name already exists.
|
||||
if (message.toLowerCase().includes("already exists")) continue;
|
||||
if (message.toLowerCase().includes("exists")) continue;
|
||||
if (message.toLowerCase().includes("already exists")) {
|
||||
continue;
|
||||
}
|
||||
if (message.toLowerCase().includes("exists")) {
|
||||
continue;
|
||||
}
|
||||
log.warn(`qmd collection add failed for ${collection.name}: ${message}`);
|
||||
}
|
||||
}
|
||||
@@ -211,9 +222,13 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
query: string,
|
||||
opts?: { maxResults?: number; minScore?: number; sessionKey?: string },
|
||||
): Promise<MemorySearchResult[]> {
|
||||
if (!this.isScopeAllowed(opts?.sessionKey)) return [];
|
||||
if (!this.isScopeAllowed(opts?.sessionKey)) {
|
||||
return [];
|
||||
}
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return [];
|
||||
if (!trimmed) {
|
||||
return [];
|
||||
}
|
||||
await this.pendingUpdate?.catch(() => undefined);
|
||||
const limit = Math.min(
|
||||
this.qmd.limits.maxResults,
|
||||
@@ -234,17 +249,21 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
log.warn(`qmd query returned invalid JSON: ${message}`);
|
||||
throw new Error(`qmd query returned invalid JSON: ${message}`);
|
||||
throw new Error(`qmd query returned invalid JSON: ${message}`, { cause: err });
|
||||
}
|
||||
const results: MemorySearchResult[] = [];
|
||||
for (const entry of parsed) {
|
||||
const doc = await this.resolveDocLocation(entry.docid);
|
||||
if (!doc) continue;
|
||||
if (!doc) {
|
||||
continue;
|
||||
}
|
||||
const snippet = entry.snippet?.slice(0, this.qmd.limits.maxSnippetChars) ?? "";
|
||||
const lines = this.extractSnippetLines(snippet);
|
||||
const score = typeof entry.score === "number" ? entry.score : 0;
|
||||
const minScore = opts?.minScore ?? 0;
|
||||
if (score < minScore) continue;
|
||||
if (score < minScore) {
|
||||
continue;
|
||||
}
|
||||
results.push({
|
||||
path: doc.rel,
|
||||
startLine: lines.startLine,
|
||||
@@ -277,7 +296,9 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
lines?: number;
|
||||
}): Promise<{ text: string; path: string }> {
|
||||
const relPath = params.relPath?.trim();
|
||||
if (!relPath) throw new Error("path required");
|
||||
if (!relPath) {
|
||||
throw new Error("path required");
|
||||
}
|
||||
const absPath = this.resolveReadPath(relPath);
|
||||
if (!absPath.endsWith(".md")) {
|
||||
throw new Error("path required");
|
||||
@@ -339,7 +360,9 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.closed) return;
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
if (this.updateTimer) {
|
||||
clearInterval(this.updateTimer);
|
||||
@@ -353,7 +376,9 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
private async runUpdate(reason: string, force?: boolean): Promise<void> {
|
||||
if (this.pendingUpdate && !force) return this.pendingUpdate;
|
||||
if (this.pendingUpdate && !force) {
|
||||
return this.pendingUpdate;
|
||||
}
|
||||
if (this.shouldSkipUpdate(force)) {
|
||||
return;
|
||||
}
|
||||
@@ -408,11 +433,15 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
stderr += data.toString();
|
||||
});
|
||||
child.on("error", (err) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
@@ -423,14 +452,18 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
private ensureDb(): SqliteDatabase {
|
||||
if (this.db) return this.db;
|
||||
if (this.db) {
|
||||
return this.db;
|
||||
}
|
||||
const { DatabaseSync } = requireNodeSqlite();
|
||||
this.db = new DatabaseSync(this.indexPath, { readOnly: true });
|
||||
return this.db;
|
||||
}
|
||||
|
||||
private async exportSessions(): Promise<void> {
|
||||
if (!this.sessionExporter) return;
|
||||
if (!this.sessionExporter) {
|
||||
return;
|
||||
}
|
||||
const exportDir = this.sessionExporter.dir;
|
||||
await fs.mkdir(exportDir, { recursive: true });
|
||||
const files = await listSessionFilesForAgent(this.agentId);
|
||||
@@ -440,15 +473,21 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
: null;
|
||||
for (const sessionFile of files) {
|
||||
const entry = await buildSessionEntry(sessionFile);
|
||||
if (!entry) continue;
|
||||
if (cutoff && entry.mtimeMs < cutoff) continue;
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
if (cutoff && entry.mtimeMs < cutoff) {
|
||||
continue;
|
||||
}
|
||||
const target = path.join(exportDir, `${path.basename(sessionFile, ".jsonl")}.md`);
|
||||
await fs.writeFile(target, this.renderSessionMarkdown(entry), "utf-8");
|
||||
keep.add(target);
|
||||
}
|
||||
const exported = await fs.readdir(exportDir).catch(() => []);
|
||||
for (const name of exported) {
|
||||
if (!name.endsWith(".md")) continue;
|
||||
if (!name.endsWith(".md")) {
|
||||
continue;
|
||||
}
|
||||
const full = path.join(exportDir, name);
|
||||
if (!keep.has(full)) {
|
||||
await fs.rm(full, { force: true });
|
||||
@@ -464,7 +503,9 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
|
||||
private pickSessionCollectionName(): string {
|
||||
const existing = new Set(this.qmd.collections.map((collection) => collection.name));
|
||||
if (!existing.has("sessions")) return "sessions";
|
||||
if (!existing.has("sessions")) {
|
||||
return "sessions";
|
||||
}
|
||||
let counter = 2;
|
||||
let candidate = `sessions-${counter}`;
|
||||
while (existing.has(candidate)) {
|
||||
@@ -477,18 +518,28 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
private async resolveDocLocation(
|
||||
docid?: string,
|
||||
): Promise<{ rel: string; abs: string; source: MemorySource } | null> {
|
||||
if (!docid) return null;
|
||||
if (!docid) {
|
||||
return null;
|
||||
}
|
||||
const normalized = docid.startsWith("#") ? docid.slice(1) : docid;
|
||||
if (!normalized) return null;
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const cached = this.docPathCache.get(normalized);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const db = this.ensureDb();
|
||||
const row = db
|
||||
.prepare("SELECT collection, path FROM documents WHERE hash LIKE ? AND active = 1 LIMIT 1")
|
||||
.get(`${normalized}%`) as { collection: string; path: string } | undefined;
|
||||
if (!row) return null;
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
const location = this.toDocLocation(row.collection, row.path);
|
||||
if (!location) return null;
|
||||
if (!location) {
|
||||
return null;
|
||||
}
|
||||
this.docPathCache.set(normalized, location);
|
||||
return location;
|
||||
}
|
||||
@@ -550,16 +601,26 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
|
||||
private isScopeAllowed(sessionKey?: string): boolean {
|
||||
const scope = this.qmd.scope;
|
||||
if (!scope) return true;
|
||||
if (!scope) {
|
||||
return true;
|
||||
}
|
||||
const channel = this.deriveChannelFromKey(sessionKey);
|
||||
const chatType = this.deriveChatTypeFromKey(sessionKey);
|
||||
const normalizedKey = sessionKey ?? "";
|
||||
for (const rule of scope.rules ?? []) {
|
||||
if (!rule) continue;
|
||||
if (!rule) {
|
||||
continue;
|
||||
}
|
||||
const match = rule.match ?? {};
|
||||
if (match.channel && match.channel !== channel) continue;
|
||||
if (match.chatType && match.chatType !== chatType) continue;
|
||||
if (match.keyPrefix && !normalizedKey.startsWith(match.keyPrefix)) continue;
|
||||
if (match.channel && match.channel !== channel) {
|
||||
continue;
|
||||
}
|
||||
if (match.chatType && match.chatType !== chatType) {
|
||||
continue;
|
||||
}
|
||||
if (match.keyPrefix && !normalizedKey.startsWith(match.keyPrefix)) {
|
||||
continue;
|
||||
}
|
||||
return rule.action === "allow";
|
||||
}
|
||||
const fallback = scope.default ?? "allow";
|
||||
@@ -567,9 +628,13 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
private deriveChannelFromKey(key?: string) {
|
||||
if (!key) return undefined;
|
||||
if (!key) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = this.normalizeSessionKey(key);
|
||||
if (!normalized) return undefined;
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
const parts = normalized.split(":").filter(Boolean);
|
||||
if (
|
||||
parts.length >= 2 &&
|
||||
@@ -581,20 +646,32 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
private deriveChatTypeFromKey(key?: string) {
|
||||
if (!key) return undefined;
|
||||
if (!key) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = this.normalizeSessionKey(key);
|
||||
if (!normalized) return undefined;
|
||||
if (normalized.includes(":group:")) return "group";
|
||||
if (normalized.includes(":channel:")) return "channel";
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (normalized.includes(":group:")) {
|
||||
return "group";
|
||||
}
|
||||
if (normalized.includes(":channel:")) {
|
||||
return "channel";
|
||||
}
|
||||
return "direct";
|
||||
}
|
||||
|
||||
private normalizeSessionKey(key: string): string | undefined {
|
||||
const trimmed = key.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseAgentSessionKey(trimmed);
|
||||
const normalized = (parsed?.rest ?? trimmed).toLowerCase();
|
||||
if (normalized.startsWith("subagent:")) return undefined;
|
||||
if (normalized.startsWith("subagent:")) {
|
||||
return undefined;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -603,7 +680,9 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
collectionRelativePath: string,
|
||||
): { rel: string; abs: string; source: MemorySource } | null {
|
||||
const root = this.collectionRoots.get(collection);
|
||||
if (!root) return null;
|
||||
if (!root) {
|
||||
return null;
|
||||
}
|
||||
const normalizedRelative = collectionRelativePath.replace(/\\/g, "/");
|
||||
const absPath = path.normalize(path.resolve(root.path, collectionRelativePath));
|
||||
const relativeToWorkspace = path.relative(this.workspaceDir, absPath);
|
||||
@@ -625,7 +704,9 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
const insideWorkspace = this.isInsideWorkspace(relativeToWorkspace);
|
||||
if (insideWorkspace) {
|
||||
const normalized = relativeToWorkspace.replace(/\\/g, "/");
|
||||
if (!normalized) return path.basename(absPath);
|
||||
if (!normalized) {
|
||||
return path.basename(absPath);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
const sanitized = collectionRelativePath.replace(/^\/+/, "");
|
||||
@@ -633,9 +714,15 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
private isInsideWorkspace(relativePath: string): boolean {
|
||||
if (!relativePath) return true;
|
||||
if (relativePath.startsWith("..")) return false;
|
||||
if (relativePath.startsWith(`..${path.sep}`)) return false;
|
||||
if (!relativePath) {
|
||||
return true;
|
||||
}
|
||||
if (relativePath.startsWith("..")) {
|
||||
return false;
|
||||
}
|
||||
if (relativePath.startsWith(`..${path.sep}`)) {
|
||||
return false;
|
||||
}
|
||||
return !path.isAbsolute(relativePath);
|
||||
}
|
||||
|
||||
@@ -646,7 +733,9 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
throw new Error("invalid qmd path");
|
||||
}
|
||||
const root = this.collectionRoots.get(collection);
|
||||
if (!root) throw new Error(`unknown qmd collection: ${collection}`);
|
||||
if (!root) {
|
||||
throw new Error(`unknown qmd collection: ${collection}`);
|
||||
}
|
||||
const joined = rest.join("/");
|
||||
const resolved = path.resolve(root.path, joined);
|
||||
if (!this.isWithinRoot(root.path, resolved)) {
|
||||
@@ -665,25 +754,33 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
const normalizedWorkspace = this.workspaceDir.endsWith(path.sep)
|
||||
? this.workspaceDir
|
||||
: `${this.workspaceDir}${path.sep}`;
|
||||
if (absPath === this.workspaceDir) return true;
|
||||
if (absPath === this.workspaceDir) {
|
||||
return true;
|
||||
}
|
||||
const candidate = absPath.endsWith(path.sep) ? absPath : `${absPath}${path.sep}`;
|
||||
return candidate.startsWith(normalizedWorkspace);
|
||||
}
|
||||
|
||||
private isWithinRoot(root: string, candidate: string): boolean {
|
||||
const normalizedRoot = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
||||
if (candidate === root) return true;
|
||||
if (candidate === root) {
|
||||
return true;
|
||||
}
|
||||
const next = candidate.endsWith(path.sep) ? candidate : `${candidate}${path.sep}`;
|
||||
return next.startsWith(normalizedRoot);
|
||||
}
|
||||
|
||||
private clampResultsByInjectedChars(results: MemorySearchResult[]): MemorySearchResult[] {
|
||||
const budget = this.qmd.limits.maxInjectedChars;
|
||||
if (!budget || budget <= 0) return results;
|
||||
if (!budget || budget <= 0) {
|
||||
return results;
|
||||
}
|
||||
let remaining = budget;
|
||||
const clamped: MemorySearchResult[] = [];
|
||||
for (const entry of results) {
|
||||
if (remaining <= 0) break;
|
||||
if (remaining <= 0) {
|
||||
break;
|
||||
}
|
||||
const snippet = entry.snippet ?? "";
|
||||
if (snippet.length <= remaining) {
|
||||
clamped.push(entry);
|
||||
@@ -698,10 +795,16 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
private shouldSkipUpdate(force?: boolean): boolean {
|
||||
if (force) return false;
|
||||
if (force) {
|
||||
return false;
|
||||
}
|
||||
const debounceMs = this.qmd.update.debounceMs;
|
||||
if (debounceMs <= 0) return false;
|
||||
if (!this.lastUpdateAt) return false;
|
||||
if (debounceMs <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (!this.lastUpdateAt) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() - this.lastUpdateAt < debounceMs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,9 @@ class FallbackMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
private async ensureFallback(): Promise<MemorySearchManager | null> {
|
||||
if (this.fallback) return this.fallback;
|
||||
if (this.fallback) {
|
||||
return this.fallback;
|
||||
}
|
||||
const fallback = await this.deps.fallbackFactory();
|
||||
if (!fallback) {
|
||||
log.warn("memory fallback requested but builtin index is unavailable");
|
||||
|
||||
Reference in New Issue
Block a user