import { clamp } from "../utils.ts"; import { isConfiguredOwnerUserId } from "../config.ts"; import { isOwnerPrivateContext } from "./memoryContext.ts"; import { isBehavioralDirectiveLikeFactText, isUnsafeMemoryFactText, isInstructionLikeFactText, normalizeFactType, normalizeMemoryLineInput } from "./memoryHelpers.ts";
type MemoryToolNamespaceScope = { ok: boolean; reason?: string; namespace?: string; guildId?: string | null; subject?: string | null; subjectIds?: string[] | null; searchScope?: "user" | "guild" | "owner" | "owner_private" | "all"; directiveScope?: "lore" | "self" | "user" | "owner"; };
type MemorySearchRow = { id?: string | number; subject?: string | null; fact?: string | null; fact_type?: string | null; score?: number | null; semanticScore?: number | null; created_at?: string | null; };
type SharedMemoryRuntime = { memory: { searchDurableFacts: (opts: { guildId?: string | null; scope?: "user" | "guild" | "owner" | "owner_private" | "all"; channelId: string | null; queryText: string; subjectIds?: string[] | null; factTypes?: string[] | null; settings: Record<string, unknown>; trace: Record<string, unknown>; limit?: number; }) => Promise<MemorySearchRow[]>; rememberDirectiveLineDetailed: (opts: { line: string; sourceMessageId: string; userId: string; guildId?: string | null; channelId: string | null; sourceText: string; scope: "lore" | "self" | "user" | "owner"; subjectOverride?: string; factType?: string | null; validationMode?: "strict" | "minimal"; }) => Promise<{ ok: boolean; reason?: string; factText?: string; subject?: string; factType?: string; isNew?: boolean; }>; }; };
type ResolveScopeArgs = { guildId?: string | null; actorUserId: string | null; namespace?: unknown; operation?: "search" | "write"; ownerPrivateContext?: boolean; };
type SearchArgs = { runtime: SharedMemoryRuntime; settings: Record<string, unknown>; guildId?: string | null; channelId?: string | null; actorUserId?: string | null; namespace?: unknown; queryText: string; trace?: Record<string, unknown>; limit?: number; tags?: string[]; };
type WriteArgs = { runtime: SharedMemoryRuntime; settings: Record<string, unknown>; guildId?: string | null; channelId?: string | null; actorUserId?: string | null; namespace?: unknown; items?: Array<{ text?: unknown; type?: unknown }>; trace?: Record<string, unknown>; sourceMessageIdPrefix?: string; sourceText?: string; limit?: number; dedupeThreshold?: number; sensitivePattern?: RegExp | null; };
const MEMORY_NAMESPACE_USER_RE = /^user:(.+)$/i; const MEMORY_NAMESPACE_GUILD_RE = /^guild:(.+)$/i; const USER_NAMESPACE_ALIASES = new Set(["speaker", "user", "me", "current_user", "current-speaker"]); const GUILD_NAMESPACE_ALIASES = new Set(["guild", "lore", "shared"]); const SELF_NAMESPACE_ALIASES = new Set(["self", "bot", "assistant"]); const OWNER_NAMESPACE_ALIASES = new Set(["owner", "private"]); const LORE_SUBJECT = "lore"; const SELF_SUBJECT = "self"; const OWNER_SUBJECT = "owner";
function resolveMemorySearchSubjectIds(rawNamespace: unknown, scope: MemoryToolNamespaceScope) { const explicitSubjectIds = Array.isArray(scope.subjectIds) ? scope.subjectIds.map((value) => String(value || "").trim()).filter(Boolean) : []; if (explicitSubjectIds.length) return explicitSubjectIds;
const normalizedNamespace = String(rawNamespace || "").trim().toLowerCase(); if (!scope.subject) return null; if (normalizedNamespace === "self" || normalizedNamespace === "bot" || normalizedNamespace === "assistant") { return [SELF_SUBJECT]; } if (normalizedNamespace === "owner" || normalizedNamespace === "private") { return [OWNER_SUBJECT]; } if (normalizedNamespace === "lore" || normalizedNamespace === "shared") { return [LORE_SUBJECT]; } return [scope.subject]; }
export function resolveMemoryToolNamespaceScope({ guildId, actorUserId, namespace = "", operation = "search", ownerPrivateContext }: ResolveScopeArgs): MemoryToolNamespaceScope { const normalizedGuildId = String(guildId || "").trim() || null; const normalizedActorUserId = String(actorUserId || "").trim() || null; const normalizedOperation = operation === "write" ? "write" : "search"; const hasGuildContext = Boolean(normalizedGuildId); const actorIsOwner = isConfiguredOwnerUserId(normalizedActorUserId); const inOwnerPrivateContext = Boolean( ownerPrivateContext ?? isOwnerPrivateContext({ guildId: normalizedGuildId, actorUserId: normalizedActorUserId }) ); const normalizedNamespace = String(namespace || "") .trim() .toLowerCase();
if (!normalizedNamespace) {
if (normalizedOperation === "write") {
if (hasGuildContext) {
return {
ok: true,
namespace: guild:${normalizedGuildId},
guildId: normalizedGuildId,
subject: LORE_SUBJECT,
subjectIds: [LORE_SUBJECT],
searchScope: "guild",
directiveScope: "lore"
};
}
if (!normalizedActorUserId) {
return {
ok: false,
reason: "actor_user_required"
};
}
return {
ok: true,
namespace: user:${normalizedActorUserId},
guildId: null,
subject: normalizedActorUserId,
subjectIds: [normalizedActorUserId],
searchScope: "user",
directiveScope: "user"
};
}
if (!hasGuildContext) {
if (!normalizedActorUserId) {
return {
ok: false,
reason: "actor_user_required"
};
}
if (inOwnerPrivateContext) {
return {
ok: true,
namespace: `owner_private:${normalizedActorUserId}`,
guildId: null,
subject: normalizedActorUserId,
subjectIds: [normalizedActorUserId, SELF_SUBJECT, OWNER_SUBJECT],
searchScope: "owner_private"
};
}
return {
ok: true,
namespace: `user:${normalizedActorUserId}`,
guildId: null,
subject: normalizedActorUserId,
subjectIds: [normalizedActorUserId, SELF_SUBJECT],
searchScope: "user"
};
}
const defaultSubjectIds = [
...new Set([normalizedActorUserId, SELF_SUBJECT, LORE_SUBJECT].filter(Boolean) as string[])
];
return {
ok: true,
namespace: `context:${normalizedGuildId}`,
guildId: normalizedGuildId,
subject: normalizedActorUserId || null,
subjectIds: defaultSubjectIds.length ? defaultSubjectIds : null,
searchScope: "all"
};
}
if (SELF_NAMESPACE_ALIASES.has(normalizedNamespace)) { return { ok: true, namespace: "self", guildId: normalizedGuildId, subject: SELF_SUBJECT, subjectIds: [SELF_SUBJECT], searchScope: "user", directiveScope: "self" }; }
if (OWNER_NAMESPACE_ALIASES.has(normalizedNamespace)) { if (!actorIsOwner) { return { ok: false, reason: "owner_required" }; } if (!inOwnerPrivateContext) { return { ok: false, reason: "owner_private_context_required" }; } return { ok: true, namespace: "owner", guildId: null, subject: OWNER_SUBJECT, subjectIds: [OWNER_SUBJECT], searchScope: "owner", directiveScope: "owner" }; }
if (USER_NAMESPACE_ALIASES.has(normalizedNamespace)) {
if (!normalizedActorUserId) {
return {
ok: false,
reason: "actor_user_required"
};
}
return {
ok: true,
namespace: user:${normalizedActorUserId},
guildId: normalizedGuildId,
subject: normalizedActorUserId,
subjectIds: [normalizedActorUserId],
searchScope: "user",
directiveScope: "user"
};
}
if (GUILD_NAMESPACE_ALIASES.has(normalizedNamespace)) {
if (!hasGuildContext) {
return {
ok: false,
reason: "guild_context_required"
};
}
return {
ok: true,
namespace: guild:${normalizedGuildId},
guildId: normalizedGuildId,
subject: LORE_SUBJECT,
subjectIds: [LORE_SUBJECT],
searchScope: "guild",
directiveScope: "lore"
};
}
if (MEMORY_NAMESPACE_GUILD_RE.test(normalizedNamespace)) {
const namespaceGuildId = normalizedNamespace.match(MEMORY_NAMESPACE_GUILD_RE)?.[1]?.trim() || "";
if (!namespaceGuildId) {
return {
ok: false,
reason: "invalid_guild_namespace"
};
}
if (!hasGuildContext) {
return {
ok: false,
reason: "guild_context_required"
};
}
if (namespaceGuildId !== normalizedGuildId) {
return {
ok: false,
reason: "guild_namespace_mismatch"
};
}
return {
ok: true,
namespace: guild:${normalizedGuildId},
guildId: normalizedGuildId,
subject: LORE_SUBJECT,
subjectIds: [LORE_SUBJECT],
searchScope: "guild",
directiveScope: "lore"
};
}
if (MEMORY_NAMESPACE_USER_RE.test(normalizedNamespace)) {
const namespaceUserId = normalizedNamespace.match(MEMORY_NAMESPACE_USER_RE)?.[1]?.trim() || "";
if (!namespaceUserId) {
return {
ok: false,
reason: "invalid_user_namespace"
};
}
return {
ok: true,
namespace: user:${namespaceUserId},
guildId: normalizedGuildId,
subject: namespaceUserId,
subjectIds: [namespaceUserId],
searchScope: "user",
directiveScope: "user"
};
}
return { ok: false, reason: "invalid_namespace" }; }
function buildMemoryToolQuery(value: unknown, maxLen: number) { return String(value || "") .replace(/\s+/g, " ") .trim() .slice(0, maxLen); }
function scoreRow(row: MemorySearchRow) { return Math.max( Number.isFinite(Number(row?.score)) ? Number(row?.score) : 0, Number.isFinite(Number(row?.semanticScore)) ? Number(row?.semanticScore) : 0 ); }
export async function executeSharedMemoryToolSearch({ runtime, settings, guildId, channelId = null, actorUserId = null, namespace = "", queryText, trace = {}, limit = 6, tags = [] }: SearchArgs) { const resolvedQuery = buildMemoryToolQuery(queryText, 220); if (!resolvedQuery) { return { ok: false, namespace: null, matches: [], error: "query_required" }; } const scope = resolveMemoryToolNamespaceScope({ guildId, actorUserId, namespace, operation: "search" }); if (!scope.ok || !scope.searchScope) { return { ok: false, namespace: null, matches: [], error: String(scope.reason || "invalid_namespace") }; }
const normalizedTags = Array.isArray(tags) ? tags.map((entry) => buildMemoryToolQuery(entry, 40)).filter(Boolean) : []; const boundedLimit = clamp(Math.floor(Number(limit) || 6), 1, 20); const searchSubjectIds = resolveMemorySearchSubjectIds(namespace, scope); const rows = await runtime.memory.searchDurableFacts({ guildId: scope.guildId || null, scope: scope.searchScope, channelId, queryText: resolvedQuery, subjectIds: searchSubjectIds, factTypes: normalizedTags.length ? normalizedTags : null, settings, trace, limit: clamp(boundedLimit * 2, 1, 40) });
const matches = (Array.isArray(rows) ? rows : []) .filter((row) => { if ( Array.isArray(searchSubjectIds) && searchSubjectIds.length > 0 && !searchSubjectIds.includes(String(row?.subject || "").trim()) ) { return false; } if (normalizedTags.length > 0 && !normalizedTags.includes(String(row?.fact_type || "").trim())) return false; return true; }) .slice(0, boundedLimit) .map((row) => ({ id: String(row?.id || ""), text: buildMemoryToolQuery(row?.fact, 420), score: Number(scoreRow(row).toFixed(3)), metadata: { createdAt: String(row?.created_at || ""), tags: [buildMemoryToolQuery(row?.fact_type, 40)].filter(Boolean) } }));
return { ok: true, namespace: scope.namespace || null, matches }; }
export async function executeSharedMemoryToolWrite({ runtime, settings, guildId, channelId = null, actorUserId = null, namespace = "", items = [], trace = {}, sourceMessageIdPrefix = "memory-tool", sourceText = "", limit = 5, dedupeThreshold = 0.9, sensitivePattern = null }: WriteArgs) { const scope = resolveMemoryToolNamespaceScope({ guildId, actorUserId, namespace, operation: "write" }); if (!scope.ok || !scope.directiveScope) { return { ok: false, namespace: null, written: [], skipped: [], error: String(scope.reason || "invalid_namespace") }; }
const normalizedItems = (Array.isArray(items) ? items : []) .map((entry) => ({ text: normalizeMemoryLineInput(entry?.text), factType: entry?.type == null ? null : normalizeFactType(entry?.type) })) .filter((entry) => Boolean(entry.text)) .slice(0, clamp(Math.floor(Number(limit) || 5), 1, 8)); if (!normalizedItems.length) { return { ok: false, namespace: scope.namespace || null, written: [], skipped: [], error: "items_required" }; }
const written = []; const skipped = []; const resolvedDedupeThreshold = clamp(Number(dedupeThreshold) || 0.9, 0, 1); const writeSearchScope = scope.directiveScope === "lore" ? "guild" : scope.directiveScope === "owner" ? "owner" : "user";
for (const [index, item] of normalizedItems.entries()) { const factType = String(item.factType || "").trim().toLowerCase(); const allowsBehavioralWrite = factType === "guidance" || factType === "behavioral"; if (sensitivePattern && sensitivePattern.test(item.text)) { skipped.push({ text: item.text, reason: "sensitive_content" }); continue; } if (isUnsafeMemoryFactText(item.text)) { skipped.push({ text: item.text, reason: "unsafe_instruction" }); continue; } if (allowsBehavioralWrite ? false : isBehavioralDirectiveLikeFactText(item.text) || isInstructionLikeFactText(item.text)) { skipped.push({ text: item.text, reason: "instruction_like" }); continue; }
const potentialDuplicates = await runtime.memory.searchDurableFacts({
guildId: scope.guildId || null,
scope: writeSearchScope,
channelId,
queryText: item.text,
subjectIds: scope.subject ? [scope.subject] : null,
factTypes: item.factType ? [item.factType] : null,
settings,
trace,
limit: 8
});
const hasDuplicate = (Array.isArray(potentialDuplicates) ? potentialDuplicates : []).some((row) => {
if (scope.subject && String(row?.subject || "").trim() !== scope.subject) return false;
return scoreRow(row) >= resolvedDedupeThreshold;
});
if (hasDuplicate) {
skipped.push({
text: item.text,
reason: "duplicate"
});
continue;
}
const sourceMessageId = `${sourceMessageIdPrefix}-${Date.now()}-${index + 1}`;
const result = await runtime.memory.rememberDirectiveLineDetailed({
line: item.text,
sourceMessageId,
userId: String(actorUserId || ""),
guildId: scope.guildId || null,
channelId,
sourceText: sourceText || item.text,
scope: scope.directiveScope,
factType: item.factType,
...((scope.directiveScope === "user" || scope.directiveScope === "owner") && scope.subject ? { subjectOverride: scope.subject } : {})
});
if (!result?.ok) {
skipped.push({
text: item.text,
reason: String(result?.reason || "write_failed")
});
continue;
}
written.push({
status: String(result.reason || "added_new"),
text: String(result.factText || item.text),
subject: String(result.subject || scope.subject || "").trim() || null
});
}
return { ok: true, namespace: scope.namespace || null, dedupeThreshold: resolvedDedupeThreshold, written, skipped }; }
