/** * Humanlike Memory Store (.hmem) * * SQLite-based long-term memory for agents with true tree structure. * L1 summaries live in the `memories` table (injected at startup). * L2+ nodes live in `memory_nodes` — each node has its own compound ID * (e.g., E0006.1, E0006.1.2) and is individually addressable. * * Two store types: * - Personal: per-agent memory (Agents/THOR/THOR.hmem) * - Company: shared knowledge base (company.hmem) with role-based access * * ID format: * Root entries: PREFIX + zero-padded sequence (e.g., P0001, L0023, T0042) * Sub-nodes: root_id + "." + sibling_seq, recursively (e.g., E0006.1, E0006.1.2) * * Prefixes: P=Project, L=Lesson, T=Task, E=Error, D=Decision, M=Milestone, S=Skill, N=Navigator * * Role hierarchy: worker < al < pl < ceo * Each entry has a min_role column (kept in DB, no longer used for filtering). * * read_memory(id) semantics: * Always returns the node + its DIRECT children only. * To go deeper, call read_memory(id=child_id). * depth parameter is IGNORED for ID-based queries. */ import Database from "better-sqlite3"; import type { HmemConfig } from "./hmem-config.js"; export type AgentRole = "worker" | "al" | "pl" | "ceo"; /** * Thrown by write_memory when similar existing entries are detected. * Handled specially by callers — surfaced as a non-error hint so the * agent can decide whether to append to an existing entry or retry with * force=true, without the UI flagging it in red. */ export declare class SimilarEntriesError extends Error { readonly bestMatch: string | undefined; constructor(message: string, bestMatch: string | undefined); } export interface MemoryEntry { id: string; prefix: string; seq: number; created_at: string; /** Short label for navigation (~30 chars). Auto-extracted if not explicit. */ title: string; level_1: string; level_2: string | null; level_3: string | null; level_4: string | null; level_5: string | null; access_count: number; last_accessed: string | null; links: string[] | null; min_role: AgentRole; /** True if the entry has been marked as no longer valid. Shown with [⚠ OBSOLETE] in reads. */ obsolete?: boolean; /** True if the agent explicitly marked this entry as a favorite. Shown with [♥] in reads. */ favorite?: boolean; /** True if the agent marked this entry as irrelevant. Hidden from bulk reads, no correction needed. */ irrelevant?: boolean; /** True if this entry is actively relevant (root-only). When any entry in a prefix has active=1, only active entries of that prefix are expanded in bulk reads. */ active?: boolean; /** ISO timestamp of last modification (write/update/append). Used for sync status. */ updated_at?: string; /** True if this entry was already delivered in a previous bulk read (session cache). */ suppressed?: boolean; /** * Set by bulk reads to indicate why this entry received extra depth inline. * 'favorite' = favorite flag set, 'access' = top-N by access_count. * Rendered as [♥] or [★] in output. */ promoted?: "access" | "favorite" | "subnode" | "task"; /** * In bulk reads: number of direct children NOT shown (only the latest child is included). * undefined = ID-based read (all direct children shown as usual). * 0 = bulk read, entry has exactly 1 child (nothing hidden). * N>0 = bulk read, N additional children exist beyond the one shown. */ hiddenChildrenCount?: number; /** True if all L2 children are shown + links resolved (V2 expanded entry). */ expanded?: boolean; /** True if this entry is a category header (seq===0, e.g. P0000). */ isHeader?: boolean; children?: MemoryNode[]; linkedEntries?: MemoryEntry[]; /** Number of linked entries hidden because they are obsolete. */ hiddenObsoleteLinks?: number; /** Number of linked entries hidden because they are irrelevant. */ hiddenIrrelevantLinks?: number; /** If this entry was reached via obsolete chain resolution, the chain of IDs traversed. */ obsoleteChain?: string[]; /** Optional hashtags for cross-cutting search, e.g. ["#hmem", "#curation"]. */ tags?: string[]; /** Entries sharing 2+ tags with this entry (populated on ID-based reads). */ relatedEntries?: { id: string; title: string; created_at: string; tags: string[]; }[]; /** True if the entry is pinned (super-favorite). Pinned entries show full L2 content in bulk reads. */ pinned?: boolean; /** FTS search: sub-nodes of this entry that matched the query. Empty/absent for root-only or tag-only matches. */ matchedNodes?: { id: string; title: string; preview: string; }[]; } export interface MemoryNode { id: string; parent_id: string; root_id: string; depth: number; seq: number; /** Short label for navigation (~30 chars). Auto-extracted from content. */ title: string; content: string; created_at: string; updated_at?: string; access_count: number; last_accessed: string | null; favorite?: boolean; irrelevant?: boolean; child_count?: number; children?: MemoryNode[]; /** Optional hashtags, e.g. ["#hmem", "#curation"]. */ tags?: string[]; } export interface ReadOptions { id?: string; depth?: number; prefix?: string; after?: string; before?: string; search?: string; limit?: number; /** @deprecated No longer used — role filtering removed. Kept for API compat. */ agentRole?: AgentRole; /** Internal: skip link resolution to prevent circular references. Default: true for ID queries. */ resolveLinks?: boolean; /** How many levels of link resolution (default 1). 0 = none. Linked entries decrement this. */ linkDepth?: number; /** Internal: visited entry IDs for cycle detection during link resolution. */ _visitedLinks?: Set; /** Include all obsolete entries in bulk reads (default: only top N most-accessed). */ showObsolete?: boolean; /** Time filter: "HH:MM" — filter entries by time of day. */ time?: string; /** Time window: "+2h", "-1h", "both" — direction and size around the time/date. */ period?: string; /** Reference entry ID — find entries created around the same time as this entry. */ timeAround?: string; /** Internal: bypass obsolete enforcement for curator tools. */ _curatorBypass?: boolean; /** Follow obsolete chains to their correction. Default: true for ID queries. */ followObsolete?: boolean; /** Show the full obsolete chain path (all intermediate entries). Default: false. */ showObsoletePath?: boolean; /** Return only titles — compact listing without V2 selection, children, or links. */ titlesOnly?: boolean; /** Expand full tree with complete node content (for deep-dive into a project). depth controls how deep. */ expand?: boolean; /** IDs already delivered in this session — shown as title-only in subsequent bulk reads. */ cachedIds?: Set; /** IDs within hidden phase (< 5 min) — completely excluded from output. */ hiddenIds?: Set; /** Slot reduction fraction: 1.0 = full, 0.5 = half percentage, 0.25 = quarter, ... */ slotFraction?: number; /** Bulk read mode: 'discover' (newest-heavy, default) or 'essentials' (importance-heavy). */ mode?: "discover" | "essentials"; /** Curation mode: show ALL entries (bypass V2 selection + session cache), depth 3 children, no child V2. */ showAll?: boolean; /** Filter by tag, e.g. "#hmem". Only entries/nodes with this tag are included. */ tag?: string; /** Show entries not accessed in the last N days (stale detection). Sorted oldest-access first. */ staleDays?: number; /** Find all entries related to a given entry via per-node tag scoring + direct links. */ contextFor?: string; /** Minimum weighted tag score for context_for matches. Default: 4. Tier weights: rare(<=5)=3, medium(6-20)=2, common(>20)=1. */ minTagScore?: number; /** Bypass V2 selection, project gate, and session cache — return all matching rows directly. Used for explicit filters (after, before, prefix, tag, stale_days). */ directResults?: boolean; } export interface WriteResult { id: string; timestamp: string; /** Compact tree summary of created L2 nodes (for agent verification). */ structure?: string; } export interface ImportResult { inserted: number; merged: number; nodesInserted: number; nodesSkipped: number; tagsImported: number; remapped: boolean; conflicts: number; } export declare class HmemStore { /** * @internal Raw SQLite handle. Reserved for migration scripts and trusted internal modules. * Application code should prefer the public methods on HmemStore — direct queries bypass * the integrity-check guard, tag handling, and FTS5 triggers' invariants. */ db: Database.Database; readonly dbPath: string; getDbPath(): string; private readonly cfg; /** True if integrity_check found errors on open (read-only mode recommended). */ readonly corrupted: boolean; /** * Char-limit tolerance: configured limits are the "recommended" target shown in skills/errors. * Actual hard reject is at limit * CHAR_LIMIT_TOLERANCE (25% buffer to avoid wasted retries). */ private static readonly CHAR_LIMIT_TOLERANCE; /** * Open (or create) a personal agent memory store. * @param hmemPath Absolute path to the `.hmem` SQLite file. * @param config Optional configuration — falls back to {@link DEFAULT_CONFIG}. */ constructor(hmemPath: string, config?: HmemConfig); /** Throw if the database is corrupted — prevents silent data loss on write operations. */ private guardCorrupted; /** * Write a new memory entry. * Content uses tab indentation to define the tree: * "Project X: built a dashboard\n\tMy role was frontend\n\t\tUsed React + Vite" * L1 (no tabs) → memories.level_1 * Each indented line → its own memory_nodes row with compound ID * Multiple lines at the same indent depth → siblings (new capability) */ write(prefix: string, content: string, links?: string[], _minRole?: AgentRole, favorite?: boolean, tags?: string[], pinned?: boolean, force?: boolean): WriteResult; /** * Write a linear entry with explicit content at each level (no tree branching). * Used by flush_context for O-prefix entries. Each level is a single node forming * a straight chain: root → .1 → .1.1 → .1.1.1 → .1.1.1.1 * * Recommended usage: L1 (title) + L2 (paragraph summary) + L5 (raw text). * L3/L4 are optional intermediate detail levels. */ writeLinear(prefix: string, levels: { l1: string; l2?: string; l3?: string; l4?: string; l5?: string; }, tags?: string[], links?: string[]): WriteResult; /** * Append a linear context chunk to an existing O-entry root. * Used by flush_context so all chunks land under the project-bound O-entry * (e.g. O0048 for P0048, O0000 for no active project) instead of creating * a new sequential root each time. * * Structure: existing root → new L2 node (l1) → .1 (l2) → .1.1 (l3) → … */ appendLinear(rootId: string, levels: { l1: string; l2?: string; l3?: string; l4?: string; l5?: string; }, tags?: string[], links?: string[]): { nodeId: string; timestamp: string; }; /** * Read memories with flexible querying. * * For ID-based queries: always returns the node + its DIRECT children. * depth parameter is ignored for ID queries (one level at a time). * * For bulk queries: returns L1 summaries (depth=1 default). */ read(opts?: ReadOptions): MemoryEntry[]; /** * Calculate V2 selection slot counts based on the number of relevant entries. * Uses percentage-based scaling with min/max caps when configured, * falls back to fixed topNewestCount/topAccessCount otherwise. */ private calcV2Slots; /** * V2 bulk-read algorithm: per-prefix expansion, smart obsolete filtering, * expanded entries with all L2 children + links. */ private readBulkV2; /** * Get all Level 1 entries for injection at agent startup. * Does NOT bump access_count (routine injection). */ getLevel1All(): string; /** * Export entire memory to Markdown for git tracking. */ exportMarkdown(): string; /** * Export memory to a new .hmem SQLite file. * Creates a standalone copy that can be opened with HmemStore or hmem.py. */ exportPublicToHmem(outputPath: string): { entries: number; nodes: number; tags: number; }; /** * Import entries from another .hmem file with L1 deduplication and ID remapping. */ importFromHmem(sourcePath: string, dryRun?: boolean): ImportResult; private _doImport; /** * Get the most recent O-entries (session logs), optionally filtered by project link. * Returns entries ordered by created_at DESC (newest first). */ getRecentOEntries(limit: number, linkedTo?: string): { id: string; title: string; created_at: string; }[]; /** * Get the last N exchanges (user message + agent response) from an O-entry. * Exchange structure: L2 = title, L4 (X.1) = user message, L5 (X.1.1) = agent response. * Returns newest first. */ getOEntryExchanges(oEntryId: string, limit: number, skipSkillDialogs?: boolean): { nodeId: string; seq: number; userText: string; agentText: string; }[]; /** * Get statistics about the memory store. */ stats(): { total: number; byPrefix: Record; totalChars: number; staleCount: number; }; /** * Per-project token size estimates for `hmem stats`. * Measures load_project payload size: level_1 + all node content/titles at depth ≤ 3. */ projectTokenStats(): Array<{ id: string; title: string; estChars: number; lastAccessed: string | null; active: number; }>; /** * Update specific fields of an existing root entry (curator use only). */ update(id: string, fields: Partial>): boolean; /** * Delete an entry by ID (curator use only). * Also deletes all associated memory_nodes. */ delete(id: string): boolean; /** * Delete a single sub-node and all its descendants by ID. * Updates FTS index via existing BEFORE DELETE trigger on memory_nodes. */ deleteNode(id: string): boolean; /** * Update the text content of an existing root entry or sub-node. * For root entries: updates level_1, optionally updates links. * For sub-nodes: updates node content only. * Does NOT modify children — use appendChildren to extend the tree. */ updateNode(id: string, newContent?: string, links?: string[], obsolete?: boolean, favorite?: boolean, curatorBypass?: boolean, irrelevant?: boolean, tags?: string[], pinned?: boolean, active?: boolean): boolean; /** * Append new child nodes under an existing entry (root or node). * Content is tab-indented relative to the parent: * 0 tabs = direct child of parentId (L_parent+1) * 1 tab = grandchild (L_parent+2), etc. * Existing children are preserved; new nodes are added after them. * Returns the IDs of newly created top-level children. */ appendChildren(parentId: string, content: string): { count: number; ids: string[]; }; /** * Append a chat exchange (user prompt + agent response) to an O-entry. * Inserts 3 nodes as a linear chain WITHOUT content parsing — newlines are preserved. * L2: title (auto-extracted from userText) * L4: user message (raw, newlines intact) * L5: agent response (raw, newlines intact) */ appendExchange(parentId: string, userText: string, agentText: string): { id: string; }; /** * Append a checkpoint summary as a tagged L2 node under an O-entry. * The summary sits alongside exchanges in the L2 sequence. * Returns the node ID. */ appendCheckpointSummary(oEntryId: string, summaryText: string): string; /** * Get checkpoint summaries for an O-entry, newest first. * Returns the summary content + the seq number (to know which exchanges it covers). */ getCheckpointSummaries(oEntryId: string, limit?: number): { nodeId: string; seq: number; content: string; created_at: string; }[]; /** * Bump access_count on a root entry or node. * Returns true if the entry was found and bumped. */ bump(id: string, increment?: number): boolean; /** * Get all header entries (seq=0) for grouped output formatting. */ getHeaders(): MemoryEntry[]; close(): void; private static readonly TAG_REGEX; private static readonly MAX_TAGS_PER_ENTRY; /** Validate and normalize tags: lowercase, must match #word pattern. */ private validateTags; /** Replace all tags on an entry/node. Pass empty array to clear. */ private setTags; /** Add a single tag to an entry/node without removing existing tags. */ addTag(entryId: string, tag: string): void; /** Find and tag untagged checkpoint summary nodes ([CP] prefix) under an O-entry. */ tagNewCheckpointSummaries(oEntryId: string): string[]; /** Get tags for a single entry/node. */ fetchTags(entryId: string): string[]; /** Bulk-fetch tags for multiple IDs at once. */ private fetchTagsBulk; /** * Find entries sharing 2+ tags with the given entry. * Returns title-only results sorted by number of shared tags (descending). */ findRelated(entryId: string, tags: string[], limit?: number): { id: string; title: string; created_at: string; tags: string[]; }[]; /** Bulk-assign tags to entries + their children from a single fetchTagsBulk call. */ assignBulkTags(entries: MemoryEntry[]): void; /** Recursively collect all node IDs from a tree of MemoryNodes. */ private collectNodeIds; /** Get root IDs that have a specific tag (for bulk-read filtering). */ private getRootIdsByTag; private migrate; /** * One-time migration: move level_2..level_5 data to memory_nodes tree. * Idempotent — tracked via schema_version table. */ private migrateToTree; /** * One-time migration: create abstract header entries (X0000) for each prefix. * Headers have seq=0 and serve as group separators in bulk reads. * Idempotent — tracked via schema_version table. */ private migrateHeaders; /** * One-time migration: reset access_count to 0 for all obsolete entries. * Entries marked obsolete before the access_count transfer feature was deployed * may still have stale access counts. This ensures obsolete entries don't * artificially surface in "top most-accessed" rankings. */ private migrateObsoleteAccessCount; /** * One-time migration: build FTS5 index from existing data. * Idempotent — tracked via schema_version key 'fts5_v1'. * For fresh DBs the triggers handle indexing; this migration covers pre-existing rows. */ private migrateFts5; /** * Add a link from sourceId to targetId (idempotent). * Only works for root entries (not nodes). */ private addLink; /** * Parse time filter "HH:MM" + date + period into start/end window. */ private parseTimeFilter; /** * Parse a time window around a reference date. * period: "+4h" (4h future), "-2h" (2h past), "4h" (±4h symmetric), "both" (±2h default) */ private parseTimeWindow; private nextSeq; /** Read-only preview of the next root ID that write() would assign for this prefix. * Used by mcp-server's id-reservation loop (multi-agent collision prevention). */ peekNextId(prefix: string): string; /** Read-only preview of the top-level child IDs that appendChildren() would create. * Used by mcp-server's sub-node reservation loop (multi-agent collision prevention). * Returns the IDs of direct children only — nested grandchildren don't need separate * reservation because they're parented under nodes this same call would create. */ peekAppendTopLevelIds(parentId: string, content: string): string[]; /** Clear all active markers — called at MCP server start so each session starts neutral. */ clearAllActive(): void; /** * Atomically set ONE project as the active P-entry in this agent's DB. * Deactivates all other P-entries in the same .hmem file. Multi-agent isolation * happens at the .hmem-file level (each agent has its own DB), so within a single * file there must only ever be one active project — otherwise getActiveProject() * (LIMIT 1) becomes nondeterministic and log-exchange routes to the wrong O-entry. */ setActiveProject(id: string, sessionId?: string): void; /** Auto-resolve linked entries on an entry (extracted for reuse in chain resolution). */ private resolveEntryLinks; /** Get child nodes created after a given ISO timestamp (for "new since last session" detection). */ getNewNodesSince(since: string, limit?: number): { id: string; root_id: string; title: string; content: string; }[]; /** Get or create the active O-entry (for log-exchange hook). */ /** Count L2 children of a root entry (direct children only). */ countDirectChildren(rootId: string): number; /** @deprecated Use resolveProjectO() instead. Will be removed in v6.0. */ getActiveO(): string; /** @deprecated Use resolveProjectO() instead. Will be removed in v6.0. Get the active O-entry ID without creating one. Returns null if none active. */ getActiveOId(): string | null; /** Get the active project entry. Returns null if none active. */ getActiveProject(sessionId?: string): { id: string; title: string; } | null; /** Get a project entry by ID. Returns null if not found or obsolete. */ getProjectById(id: string): { id: string; title: string; } | null; /** * Heuristic fallback: find the project whose O-entry received the most recent * exchange. Used by cli-checkpoint when session-marker + active=1 flag both miss * (e.g. multi-device sync stripped the marker, or /clear wiped it). */ getMostRecentlyActiveProject(): { id: string; title: string; } | null; /** * Get the second-to-last session (L2 node) under an O-entry. * Used by the SessionStart hook to check if the previous session needs a summary. * Returns null if fewer than 2 sessions exist. */ getPreviousSession(oId: string): { id: string; title: string; content: string; } | null; /** * Read a root entry from the memories table by ID. Returns null if not found. */ readEntry(id: string): { id: string; prefix: string; seq: number; level_1: string; links: string | null; } | null; /** True if the entry with `id` exists and is flagged obsolete. */ isObsolete(id: string): boolean; /** True if there is at least one entry with the given prefix that is active and neither irrelevant nor obsolete. */ hasActiveEntryWithPrefix(prefix: string): boolean; /** Return the title of a non-obsolete entry, or undefined if missing or obsolete. * Falls back to level_1 (body) when title is NULL or stored as the ID itself — * some entries created via write_memory put the descriptive content in the body * while title ends up empty or equal to the ID. */ getNonObsoleteTitle(id: string): string | undefined; /** Get the display title of any entry or sub-node by ID. Used by update_memory body-only mode. */ getTitle(id: string): string | null; /** * Find or create the O-entry for a given project sequence number. * O0048 belongs to P0048, O0000 is the non-project catch-all. * Does NOT use the active flag — O is derived purely from P's seq. */ resolveProjectO(projectSeq: number): string; /** * Find or create a session (L2 node) under an O-entry. * Sessions are tracked via a temp file keyed by O-entry ID hash. * A new transcript_path means a new Claude Code session. */ resolveSession(oId: string, transcriptPath: string): string; /** * Find or create a batch (L3 node) under a session. * Creates a new batch if the current one is full (has >= batchSize L4 children). */ resolveBatch(sessionId: string, oId: string, batchSize: number): string; /** * Append a V2 exchange (3-node chain) under a batch (L3 node). * Creates: * L4: exchange node — title auto-extracted from userText * L5.1: user message (raw userText) * L5.2: agent message (raw agentText) */ appendExchangeV2(batchId: string, oId: string, userText: string, agentText: string): { id: string; }; countBatchExchanges(batchId: string): number; /** * Process pending exchanges queued by hooks that couldn't open the DB * (e.g. Windows WAL locking when MCP server holds the DB). * File: {hmemDir}/pending-exchanges.jsonl — one JSON object per line. */ processPendingExchanges(): number; getOEntryExchangesV2(oId: string, limit: number, opts?: { skipIrrelevant?: boolean; titleOnlyTags?: string[]; sessionScope?: string[]; }): { nodeId: string; title: string; userText: string; agentText: string; created_at: string; }[]; /** * Find the latest full batch (L3 node with >= batchSize L4 children) * under a given O-entry root. Returns null if no full batch exists. */ getLatestFullBatch(oId: string, batchSize: number): { id: string; sessionId: string; } | null; /** * Find orphaned batches — L3 nodes with no checkpoint summary, from sessions other than * excludeSessionId, that have at least one L4 exchange. Returns up to cap, oldest first. */ getOrphanedBatches(oId: string, excludeSessionId: string | null, cap?: number): Array<{ batchId: string; sessionId: string; sessionTitle: string; sessionDate: string; }>; /** * Get the previous batch (L3) sibling within the same session, excluding a given batch. * Returns the batch's id, content, and title. */ getPreviousBatch(sessionId: string, excludeBatchId: string): { id: string; content: string; title: string; } | null; /** Read a single memory_nodes row by ID. Returns null if not found. */ readNode(id: string): MemoryNode | null; /** Return all direct children of a node, ordered by seq. */ getChildNodes(parentId: string): MemoryNode[]; /** Find a child node by content/title pattern. Returns node ID or null. */ findChildNode(parentId: string, pattern: string, depth?: number): string | null; /** Find a child node of a root entry by content/title pattern. */ findRootChildNode(rootId: string, pattern: string, depth: number): string | null; /** * Return all non-obsolete P-entries with just id + title. */ listProjects(): { id: string; title: string; }[]; /** * Move L2 (sessions), L3 (batches), or L4 (exchanges) between O-entries. * Rewrites all IDs in the subtree (node + children + tags + FTS). */ moveNodes(nodeIds: string[], targetOId: string): { moved: number; errors: string[]; }; /** * Find an existing L2 session created on the given date, or create a new one. */ private _findOrCreateSessionForDate; /** * Find a batch under the session with room, or create a new one. */ private _findOrCreateBatchForDate; /** * Move a subtree (node + all descendants) to a new parent in the target O-entry. * Rewrites all IDs, parent_ids, root_ids, tags, and FTS rowid map entries. */ private _moveSubtree; /** * Rename an entire L2 session subtree (L2 node + all L3/L4/L5 descendants) * to a new id prefix. Updates memory_nodes (id, parent_id, seq), memory_tags, * and hmem_fts_rowid_map. The root-level parent of the L2 node stays at oId. * Caller must ensure newId does not yet exist. */ private _renameL2Subtree; /** * Reorder L2 sessions under an O-entry so their seq matches chronological * order by created_at (ascending). Uses 2-phase rename via _TMP staging IDs * to avoid collisions during renumbering. Returns the number of sessions * actually renamed. */ reorderSessionsByDate(oId: string): number; /** * Remove empty L2 (sessions) and L3 (batches) nodes in an O-entry. */ private _cleanupEmptyParents; bumpAccess(id: string): void; /** * Auto-purge: physically delete irrelevant entries older than maxAgeDays. * Only deletes entries where irrelevant=1 — entries rescued by bumpAccess survive. * Returns the number of deleted entries. */ purgeIrrelevant(maxAgeDays?: number): number; /** * Atomically rename an entry ID and update all references across the database. * Used to resolve ID conflicts after sync-push detects a collision. * * Updates: memories.id, memory_nodes (id, parent_id, root_id), * memory_tags.entry_id, hmem_fts_rowid_map (root_id, node_id), * memories.links (JSON arrays in other entries), level_1 obsolete markers [✓ID]. * * Returns the number of affected rows (nodes + link rewrites + tag rewrites). */ renameId(oldId: string, newId: string): { ok: boolean; affected: number; error?: string; }; private bumpNodeAccess; /** * Follow the obsolete chain from an entry to its final valid correction. * Parses [✓ID] from level_1 of each obsolete entry and follows the chain. * Returns the final (non-obsolete) entry ID and the full chain of IDs traversed. */ private resolveObsoleteChain; /** * Rewrite all external links that reference `obsoleteId` to point to `correctionId` instead. * Called automatically when an entry is marked obsolete with a [✓ID] correction reference. * Skips the obsolete entry itself and its correction (those are handled via addLink). */ private rewriteLinksToObsolete; /** Fetch direct children of a node (root or compound), including their grandchild counts. */ /** Bulk-fetch direct child counts for multiple parent IDs in one query. */ private bulkChildCount; /** * Time-weighted access score: newer entries with fewer accesses can outrank * older entries with more accesses. Uses logarithmic age decay: * score = access_count / log2(age_in_days + 2) */ private weightedAccessScore; private fetchChildren; /** * Fetch only the single most recently created direct child of a parent, * along with the total sibling count. Used for token-efficient bulk reads. * Returns null if no children exist. */ private fetchLatestChild; /** * Fetch children recursively up to maxDepth. * currentDepth: the depth level of the children being fetched (2 = L2, 3 = L3, …) * maxDepth: stop recursing when currentDepth > maxDepth */ private fetchChildrenDeep; private rowToNode; private rowToEntry; /** * Wrap a MemoryNode as a MemoryEntry for uniform API return. * The formatter detects node entries by checking e.id.includes("."). * level_1 is repurposed to carry the node content. */ private nodeToEntry; /** * Auto-extract a short title from text. * Priority: text before " — " > word-boundary truncation > hard truncation. */ private autoExtractTitle; /** * Parse tab-indented content into title + L1 text + a list of tree nodes. * * Title extraction: * - 2+ non-indented lines: first line = explicit title, rest = level_1 * - 1 non-indented line: title = auto-extracted (~30 chars), level_1 = full line * * Algorithm: * - seqAtParent: Map — sibling counter per parent * - lastIdAtDepth: Map — last-written node id at each depth * * Each indented line at depth D: * parent = (D == 2) ? rootId : lastIdAtDepth[D-1] * seq = ++seqAtParent[parent] * id = parent + "." + seq * * @param content Tab-indented content string * @param rootId The root entry ID (e.g. "E0006") — used to build compound IDs */ private parseTree; /** * Parse tab-indented content relative to a parent node. * relDepth 0 = direct child of parent (absDepth = parentDepth + 1). * startSeq: the first seq number to assign to direct children (continuing after existing siblings). */ private parseRelativeTree; /** Return a statistical overview of the memory store. */ getStats(): { totalEntries: number; byPrefix: Record; totalNodes: number; favorites: number; pinned: number; mostAccessed: { id: string; title: string; access_count: number; }[]; oldestEntry: { id: string; created_at: string; title: string; } | null; staleCount: number; uniqueTags: number; avgDepth: number; }; /** * Find entries similar to the given entry via FTS5 keyword matching. * Extracts significant words from level_1, queries FTS5, returns up to `limit` results. */ findRelatedCombined(entryId: string, limit?: number): { id: string; title: string; created_at: string; tags: string[]; matchType: "tags" | "fts"; }[]; findRelatedByFts(entryId: string, limit?: number): { id: string; title: string; created_at: string; tags: string[]; }[]; /** * Find all entries contextually related to a given entry. * Uses per-node weighted tag scoring: for each node of the source entry, * compute weighted overlap with each candidate entry's full tag set. * Tier weights: rare (<=5 entries) = 3, medium (6-20) = 2, common (>20) = 1. * A candidate matches if ANY source node scores >= minTagScore against it. * Bidirectional direct links are always included. */ findContext(entryId: string, minTagScore?: number, limit?: number): { linked: MemoryEntry[]; tagRelated: { entry: MemoryEntry; score: number; matchNode: string; }[]; }; /** Resolve bidirectional direct links for an entry + all subnodes, filtering obsolete/irrelevant. */ private resolveDirectLinks; /** Audit report: broken links, orphaned entries, stale favorites, broken obsolete chains, tag orphans. */ healthCheck(): { brokenLinks: { id: string; title: string; brokenIds: string[]; }[]; orphanedEntries: { id: string; title: string; created_at: string; }[]; staleFavorites: { id: string; title: string; lastAccessed: string | null; }[]; brokenObsoleteChains: { id: string; title: string; badRef: string; }[]; tagOrphans: number; }; /** * Apply tag changes (add/remove) to all entries matching a filter. * Returns the number of entries modified. */ tagBulk(filter: { prefix?: string; search?: string; tag?: string; }, addTags?: string[], removeTags?: string[]): number; /** * Rename a tag across all entries and nodes. * Returns the number of rows updated. */ tagRename(oldTag: string, newTag: string): number; /** * Move a sub-node (and its entire subtree) to a different parent. * sourceId must be a sub-node (e.g. "P0029.15"), not a root entry. * targetParentId can be a root (e.g. "L0074") or a sub-node (e.g. "P0029.20"). * All IDs in links and [✓ID] content references are updated automatically. */ moveNode(sourceId: string, targetParentId: string): { moved: number; newId: string; idMap: Record; }; } /** * Resolve the `.hmem` file path for the current agent. * Priority: HMEM_PATH env var > CWD discovery > ~/.hmem/memory.hmem * Priority: `HMEM_PATH` env var → CWD discovery → `~/.hmem/agent.hmem`. * @param cwdOverride Override working directory for CWD discovery step. */ export declare function resolveHmemPath(cwdOverride?: string): string; /** * Open (or create) the shared company knowledge store (`company.hmem`). * @param projectDir Directory that contains (or will contain) `company.hmem`. * @param config Optional configuration — falls back to {@link DEFAULT_CONFIG}. */ export declare function openCompanyMemory(projectDir: string, config?: HmemConfig): HmemStore;