mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-06-01 22:11:08 +02:00
refactor: make memory dedupe repo-agnostic
This commit is contained in:
+67
-68
@@ -313,72 +313,80 @@ export function workspaceMemoryExactKey(entry: Pick<LongTermMemoryEntry, "type"
|
||||
return `${entry.type}:${canonicalMemoryText(entry.text)}`;
|
||||
}
|
||||
|
||||
/** Extract entity/destination keys for project and reference dedup */
|
||||
function extractEntityKey(text: string): string | null {
|
||||
const normalized = canonicalMemoryText(text);
|
||||
// Check known key phrases (bilingual-friendly)
|
||||
// opencode + agenthub plugin system
|
||||
if (/opencode.*agenthub/i.test(normalized)) {
|
||||
return "opencode-agenthub plugin system";
|
||||
function normalizeUrlIdentity(raw: string): string | null {
|
||||
const cleaned = raw.replace(/[),.;:!?]+$/g, "");
|
||||
try {
|
||||
const url = new URL(cleaned);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
||||
url.protocol = url.protocol.toLowerCase();
|
||||
url.hostname = url.hostname.toLowerCase();
|
||||
url.hash = "";
|
||||
if (url.pathname.length > 1) {
|
||||
url.pathname = url.pathname.replace(/\/+$/g, "");
|
||||
}
|
||||
return `url:${url.toString()}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
// For generic config references, fall back to canonical text dedup — no entity key
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Extract decision topic key for supersession detection */
|
||||
function decisionTopicKey(text: string): string | null {
|
||||
const normalized = text.toLowerCase();
|
||||
// Parser format versions
|
||||
if (/parser.*formats?|supports?\s*\d+\s*format/i.test(normalized)) {
|
||||
return "parser-supported-formats";
|
||||
}
|
||||
// Compaction template replacement
|
||||
if (/compaction.*template|output\.prompt|template.*replace/i.test(normalized)) {
|
||||
return "compaction-template-replacement";
|
||||
}
|
||||
// Plugin loading
|
||||
if (/plugin.*load|npm.*cache|plugin.*config/i.test(normalized)) {
|
||||
return "plugin-loading-config";
|
||||
}
|
||||
// Output format changes (purple/italic, YAML frontmatter, etc)
|
||||
if (/purple.*italic|markup|markdown.*render|frontmatter/i.test(normalized)) {
|
||||
return "output-format-rendering";
|
||||
}
|
||||
return null;
|
||||
function normalizePathIdentity(raw: string): string | null {
|
||||
const unwrapped = raw
|
||||
.trim()
|
||||
.replace(/^[`"']+|[`"']+$/g, "")
|
||||
.replace(/[),.;:!?]+$/g, "")
|
||||
.replace(/\\+/g, "/");
|
||||
|
||||
if (!unwrapped) return null;
|
||||
const collapsed = unwrapped.startsWith("/")
|
||||
? `/${unwrapped.slice(1).replace(/\/+$/g, "/").replace(/\/+/g, "/")}`
|
||||
: unwrapped.replace(/\/+/g, "/");
|
||||
const withoutTrailingSlash = collapsed.length > 1 ? collapsed.replace(/\/+$/g, "") : collapsed;
|
||||
return `path:${withoutTrailingSlash}`;
|
||||
}
|
||||
|
||||
/** Extract feedback topic key for supersession detection */
|
||||
function feedbackTopicKey(text: string): string | null {
|
||||
const normalized = text.toLowerCase();
|
||||
// Purple/italic rendering issue
|
||||
if (/purple.*italic/i.test(normalized)) {
|
||||
return "purple-italic-rendering";
|
||||
function isConcretePathIdentity(pathIdentity: string): boolean {
|
||||
const path = pathIdentity.slice("path:".length);
|
||||
if (!path || path === "." || path === "..") return false;
|
||||
|
||||
if (path.startsWith("/")) return true;
|
||||
if (/^\.\.?\//.test(path)) return true;
|
||||
if (/^\.[A-Za-z0-9_.-]+\//.test(path)) return true;
|
||||
if (/^[A-Za-z0-9_.-]+\//.test(path)) return true;
|
||||
return /\.(?:json|jsonc|ts|tsx|js|jsx|mjs|cjs|md|yaml|yml|toml|lock|config)$/i.test(path);
|
||||
}
|
||||
|
||||
function normalizeConcretePathIdentity(raw: string): string | null {
|
||||
const pathIdentity = normalizePathIdentity(raw);
|
||||
if (!pathIdentity) return null;
|
||||
return isConcretePathIdentity(pathIdentity) ? pathIdentity : null;
|
||||
}
|
||||
|
||||
function extractConcreteIdentityKey(text: string): string | null {
|
||||
const urlMatch = text.match(/https?:\/\/[^\s`"'<>]+/i);
|
||||
if (urlMatch) {
|
||||
const urlIdentity = normalizeUrlIdentity(urlMatch[0]);
|
||||
if (urlIdentity) return urlIdentity;
|
||||
}
|
||||
// Browser login/server errors (500 internal_error)
|
||||
if (/login.*500|500.*internal|internal_error|server.*error/i.test(normalized)) {
|
||||
return "server-error";
|
||||
|
||||
const wrappedPathPattern = /[`"']([^`"']+)[`"']/g;
|
||||
for (const match of text.matchAll(wrappedPathPattern)) {
|
||||
const pathIdentity = normalizeConcretePathIdentity(match[1]);
|
||||
if (pathIdentity) return pathIdentity;
|
||||
}
|
||||
// Port occupied / environment issues
|
||||
if (/port.*occup|9473|端口|舊進程|旧进程/i.test(normalized)) {
|
||||
return "port-occupied-environment";
|
||||
}
|
||||
// Theme preferences
|
||||
if (/theme|dark.*light|prefer.*theme/i.test(normalized)) {
|
||||
return "theme-preference";
|
||||
}
|
||||
return null;
|
||||
|
||||
const pathMatch = text.match(/(?:\/[^ | ||||