/** * tm_memory — THREE-layer memory store (global / project / session), * Qoder-inspired, re-implemented from mechanism analysis; no code reused. * * Design: memories are plain Markdown files with a small YAML-ish * frontmatter (title / usage_scenario / keywords / supersedes / created_at / * updated_at / expires) — git-diffable, human-editable, injectable into * context without any database. The store lives under the git-aware base * (/.git/opencode-team/memories/ or tmpdir fallback), mirroring the * run-store philosophy: never pollute the working tree. * * Layout: * GLOBAL (user-level, follows you across projects): * //.md — default ~/.opencode-team/memories/global/ * PROJECT (repo-level, per checkout): * <storeBase>/memories/projects/<project-slug>/<category>/<title>.md * SESSION (per ctx.sessionID — EPHEMERAL by default): * in-process Map keyed by the session id; nothing touches the disk * unless TM_MEMORY_SESSION_PERSIST=1, which mirrors every session entry * to <storeBase>/memories/sessions/<sid>/<category>/<title>.md with an * `expires:` frontmatter line. Expired entries die on a lazy per-call * sweep AND a startup sweep (never read back after the TTL). * COMPACT backups (rollback path, never walked — dot-dirs are skipped): * <storeBase>/memories/.compact-backup/<UTC ts>/<scope>/<rel path> * * Precedence: session > project > global. search scores every layer it is * asked to walk; same-title lower-layer entries are shadowed by the highest * layer that carries them, and project gets a +2 / session a +3 near-tie * weight so a layer's entries rank first. * * Bloat control (the root cause this tier fixes: near-identical memories * piling up as separate files): * - add NEVER creates a second entry that duplicates an existing one in * the same scope+category: identity is `category + sorted(stopword- * stripped title tokens)` OR a Jaccard(title∪keywords tokens) ≥ 0.6 — * either match MERGES into the existing file (content takes the new * value, keywords union, `supersedes:` grows, updated_at refreshes). * - over cfg.memoryMaxEntries per scope, add fails with a compact/forget * hint instead of growing the store. * - cfg.memoryStaleDays > 0 tags search hits `[stale Nd]`. * - action "compact" groups the existing duplicates: dry-run by default, * `apply: true` merges for real after moving originals to the backup dir. * * The project slug is derived from the workspace path (drive letter + path * segments dashed, lowercased) so two checkouts never share a memory set. * Injection is PULL-based: agents search via this tool; the lead distills * relevant memories into dispatches (see SHARED_RULES). */ import type { ToolResult } from "../types.js"; import { type TmConfig } from "./config.js"; import type { TmPipelines } from "./pipelines.js"; /** Hard caps — a memory that cannot fit a context budget is a doc, not a memory. */ export declare const MEMORY_TITLE_MAX = 120; export declare const MEMORY_CONTENT_MAX = 4000; export declare const MEMORY_SEARCH_RESULTS = 5; export declare const MEMORY_EXCERPT_MAX = 600; /** Layered precedence: session > project > global — (a) same-title entries in * a lower layer are shadowed by the highest layer carrying them, (b) session * entries get a +3 and project entries a +2 near-tie weight (matches * category ×2 in the scoring grammar). */ export declare const MEMORY_PROJECT_SCOPE_BONUS = 2; export declare const MEMORY_SESSION_SCOPE_BONUS = 3; /** Near-duplicate merge threshold on title∪keywords token sets. */ export declare const MEMORY_DEDUP_JACCARD = 0.6; /** Per-list frontmatter caps (kept stable so merges cannot grow unbounded). */ export declare const MEMORY_KEYWORDS_MAX = 10; export declare const MEMORY_SCENARIOS_MAX = 8; export declare const MEMORY_SUPERSEDES_MAX = 50; /** Default session id when the host ctx carries no sessionID. */ export declare const MEMORY_DEFAULT_SESSION_ID = "default"; /** Seeded category taxonomy (Qoder's seven, reused as the starting point). */ export declare const MEMORY_CATEGORIES: readonly string[]; /** The three memory layers. */ export type MemoryScope = "project" | "global" | "session"; export declare const MEMORY_SCOPES: readonly MemoryScope[]; /** Layer precedence: session > project > global (also drives same-title shadowing). */ export declare const MEMORY_ALL_SCOPES: MemoryScope[]; /** Path → project slug ("D:\Github\App" -> "d-github-app"). */ export declare function projectSlug(directory: string): string; /** Title → safe file stem (collapsed dashes, capped). */ export declare function titleSlug(title: string): string; /** Session id → path-safe dir name. */ export declare function sessionIdSlug(sid: unknown): string; /** * Deterministic tokenizer: latin/digit runs + one token per CJK glyph * (Chinese titles have no whitespace, so glyph tokens are what make the * Jaccard signal work). Stopwords removed. */ export declare function memoryTokens(text: unknown): string[]; /** * Identity key of a memory: category + the sorted token set of its title. * Two entries with the same key in the same scope+category are the same fact. */ export declare function memoryDedupKey(category: string, title: string): string; /** Jaccard similarity of two token sets; empty-vs-empty is 0 (no signal). */ export declare function memoryJaccard(a: string[], b: string[]): number; export declare function renderMemoryMarkdown(m: { title: string; usageScenario: string[]; keywords: string[]; content: string; /** Forward-compatible extras — old readers ignore unknown keys. */ supersedes?: string[]; createdAt?: number | null; updatedAt?: number | null; expiresAt?: number | null; }): string; /** Parse a memory file; null when the file is not ours (foreign md). */ export declare function parseMemoryMarkdown(raw: string, filePath: string): { title: string; usageScenario: string[]; keywords: string[]; content: string; filePath: string; category: string; supersedes: string[]; createdAt: number | null; updatedAt: number | null; expiresAt: number | null; } | null; export interface MemoryHit { title: string; category: string; scope: MemoryScope; filePath: string; keywords: string[]; usageScenario: string[]; content: string; score: number; /** Merge / staleness / TTL bookkeeping (additive — older readers ignore). */ slug: string; createdAt: number; updatedAt: number; expiresAt: number | null; supersedes: string[]; sid: string; } /** * Substring scoring — no embeddings, deterministic: title ×5, keywords ×4, * usage_scenario ×3, category ×2, body ×1 (query tokenized on whitespace). */ export declare function scoreMemory(m: { title: string; keywords: string[]; usageScenario: string[]; category: string; content: string; }, query: string): number; export declare function buildTmMemoryTool(deps: { storeBase: string; /** User-level global memory dir (default ~/.opencode-team/memories/global; * TM_MEMORY_GLOBAL_DIR override). Lives OUTSIDE any repo so "global" * really follows the user across projects. */ globalRoot: string; directory: string; cfg: TmConfig; pipelines: TmPipelines; args?: Record<string, unknown>; /** Clock seam for tests (TTL / staleness); defaults to Date.now. */ now?: () => number; }): { description: string; args: Record<string, unknown>; execute: (rawArgs: Record<string, unknown>, ctx: unknown) => Promise<ToolResult>; }; //# sourceMappingURL=memory.d.ts.map