refactor(memory): extract qmd scope helpers

This commit is contained in:
Peter Steinberger
2026-02-13 17:00:54 +00:00
parent c256503ea1
commit ca3a42009c
2 changed files with 82 additions and 77 deletions

View File

@@ -14,7 +14,7 @@ import type {
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
import { resolveStateDir } from "../config/paths.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { parseAgentSessionKey } from "../sessions/session-key-utils.js";
import { deriveQmdScopeChannel, deriveQmdScopeChatType, isQmdScopeAllowed } from "./qmd-scope.js";
import {
listSessionFilesForAgent,
buildSessionEntry,
@@ -751,89 +751,17 @@ export class QmdMemoryManager implements MemorySearchManager {
}
}
private isScopeAllowed(sessionKey?: string): boolean {
const scope = this.qmd.scope;
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;
}
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;
}
return rule.action === "allow";
}
const fallback = scope.default ?? "allow";
return fallback === "allow";
}
private logScopeDenied(sessionKey?: string): void {
const channel = this.deriveChannelFromKey(sessionKey) ?? "unknown";
const chatType = this.deriveChatTypeFromKey(sessionKey) ?? "unknown";
const channel = deriveQmdScopeChannel(sessionKey) ?? "unknown";
const chatType = deriveQmdScopeChatType(sessionKey) ?? "unknown";
const key = sessionKey?.trim() || "<none>";
log.warn(
`qmd search denied by scope (channel=${channel}, chatType=${chatType}, session=${key})`,
);
}
private deriveChannelFromKey(key?: string) {
if (!key) {
return undefined;
}
const normalized = this.normalizeSessionKey(key);
if (!normalized) {
return undefined;
}
const parts = normalized.split(":").filter(Boolean);
if (
parts.length >= 2 &&
(parts[1] === "group" || parts[1] === "channel" || parts[1] === "direct" || parts[1] === "dm")
) {
return parts[0]?.toLowerCase();
}
return undefined;
}
private deriveChatTypeFromKey(key?: string) {
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";
}
return "direct";
}
private normalizeSessionKey(key: string): string | undefined {
const trimmed = key.trim();
if (!trimmed) {
return undefined;
}
const parsed = parseAgentSessionKey(trimmed);
const normalized = (parsed?.rest ?? trimmed).toLowerCase();
if (normalized.startsWith("subagent:")) {
return undefined;
}
return normalized;
private isScopeAllowed(sessionKey?: string): boolean {
return isQmdScopeAllowed(this.qmd.scope, sessionKey);
}
private toDocLocation(

77
src/memory/qmd-scope.ts Normal file
View File

@@ -0,0 +1,77 @@
import type { ResolvedQmdConfig } from "./backend-config.js";
import { parseAgentSessionKey } from "../sessions/session-key-utils.js";
export function isQmdScopeAllowed(scope: ResolvedQmdConfig["scope"], sessionKey?: string): boolean {
if (!scope) {
return true;
}
const channel = deriveQmdScopeChannel(sessionKey);
const chatType = deriveQmdScopeChatType(sessionKey);
const normalizedKey = sessionKey ?? "";
for (const rule of scope.rules ?? []) {
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;
}
return rule.action === "allow";
}
const fallback = scope.default ?? "allow";
return fallback === "allow";
}
export function deriveQmdScopeChannel(key?: string): string | undefined {
if (!key) {
return undefined;
}
const normalized = normalizeQmdSessionKey(key);
if (!normalized) {
return undefined;
}
const parts = normalized.split(":").filter(Boolean);
if (
parts.length >= 2 &&
(parts[1] === "group" || parts[1] === "channel" || parts[1] === "direct" || parts[1] === "dm")
) {
return parts[0]?.toLowerCase();
}
return undefined;
}
export function deriveQmdScopeChatType(key?: string): "channel" | "group" | "direct" | undefined {
if (!key) {
return undefined;
}
const normalized = normalizeQmdSessionKey(key);
if (!normalized) {
return undefined;
}
if (normalized.includes(":group:")) {
return "group";
}
if (normalized.includes(":channel:")) {
return "channel";
}
return "direct";
}
function normalizeQmdSessionKey(key: string): string | undefined {
const trimmed = key.trim();
if (!trimmed) {
return undefined;
}
const parsed = parseAgentSessionKey(trimmed);
const normalized = (parsed?.rest ?? trimmed).toLowerCase();
if (normalized.startsWith("subagent:")) {
return undefined;
}
return normalized;
}