Lint: add braces for single-line ifs

This commit is contained in:
Benjamin Jesuiter
2026-02-02 22:56:20 +01:00
committed by Vignesh
parent 23cfcd60df
commit e332a717a8
7 changed files with 227 additions and 76 deletions

View File

@@ -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 gateways `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 cant be

View File

@@ -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);

View File

@@ -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";
}

View File

@@ -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" },

View File

@@ -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");

View File

@@ -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;
}
}

View File

@@ -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");