Architecture¶
HackLM Memory is a VS Code extension that gives GitHub Copilot persistent, long-term memory across sessions. Memory is stored as Markdown bullet lists in a .memory/ folder inside the workspace. A managed block in .github/copilot-instructions.md points the Copilot agent to those files so it fetches only what it needs, on demand.
Data Flow¶
flowchart TD
A[Copilot Chat] -->|calls storeMemory tool| B[StoreMemoryTool]
B --> C{Jaccard dedup check\ndedup.ts}
C -->|skip| Z[No write]
C -->|store / update| D{LLM redundancy check\nlm.ts}
D -->|redundant| Z
D -->|unique| E[markdownStore.ts\nupsert / append]
E -->|file written| F[.memory/category.md]
F -->|FileSystemWatcher| G[Status bar refresh\nTree view refresh]
F -->|on activation| H[instructionFiles.ts\nupsert block in\n.github/copilot-instructions.md]
H --> I[copilot-instructions.md\npoints agent to .memory/ files]
A2[Copilot Chat] -->|calls queryMemory tool| J[QueryMemoryTool]
J --> K[readAllMemories\nmarkdownStore.ts]
K --> L[searchMemories\nsearch.ts]
L -->|ranked results| A2
M[User: Run Cleanup] --> N[cleanupMemory.ts]
N --> O[findSimilarClusters\ndedup.ts]
O --> P[scoreAllEntries\nscoring.ts]
P --> Q[Prune, merge, slug-assign\nmarkdownStore.ts]
Module Map¶
Entry Point¶
| File | Responsibility |
|---|---|
extension/src/extension.ts |
Activates the extension. Registers LM tools, commands, tree view, status bar, file watcher. Shows consent toast on first run. Opens walkthrough on first activation. |
Storage Layer (extension/src/storage/)¶
| File | Responsibility |
|---|---|
markdownStore.ts |
Read/write/delete memory entries from .memory/*.md files. Per-file mutex locks prevent concurrent read-modify-write races. Parses - [slug] content bullet format. |
dedup.ts |
Jaccard similarity deduplication. SKIP_THRESHOLD=0.8, UPDATE_THRESHOLD=0.6. Keyword extraction strips stop words. Also provides findSimilarClusters() for cleanup. |
scoring.ts |
Scores entries by category weight + brevity + specificity. Used by cleanup to prune lowest-value entries when a category exceeds its limit. |
search.ts |
Keyword-based search for queryMemory. Scores by keyword hit count + exact phrase bonus. Returns results ranked by score. |
LM Tools (extension/src/tools/)¶
| File | Responsibility |
|---|---|
storeMemory.ts |
StoreMemoryTool — dedup check → optional LLM redundancy check → upsert or append. Triggers gap analysis every N stores. Shows confirmation prompt unless autoApproveStore is enabled. |
queryMemory.ts |
QueryMemoryTool — reads all memories, runs keyword search, formats results as [Category] content lines. |
cleanupMemory.ts |
runCleanup() — normalises raw lines, merges similar clusters (threshold 0.3), prunes over-limit entries by score, assigns slugs to untagged entries. |
sessionReview.ts |
triggerGapAnalysis() fires every 3rd store. runSessionReview() is user-invoked. Both use an LLM to find implied but uncaptured decisions and prompt the user to accept suggestions. |
Infrastructure¶
| File | Responsibility |
|---|---|
lm.ts |
Single place for all LM calls. resolveModel() picks the configured model family. sendLmRequest() wraps sendRequest with cancellation token forwarding, timeout, and error logging. |
instructionFiles.ts |
Creates and upserts the <!-- hacklm-memory:start/end --> block in .github/copilot-instructions.md. Bootstraps .memory/ files on first open. Strips legacy blocks. |
memoryPanel.ts |
QuickPick control panel — browse, delete, cleanup, settings. Settings UI lets user pick LM family from available Copilot models. |
memoryTreeView.ts |
MemoryTreeProvider — shows memories grouped by category in Explorer sidebar. CategoryItem and MemoryItem tree items with context-menu commands. |
statusBar.ts |
Left-aligned status bar item showing Memory (N). Refreshes on file watcher events and on onDidChangeChatModels. |
toolIds.ts |
Single source of truth for LM tool name constants. |
utils.ts |
getEffectiveLimit(category) — reads per-category limit from VS Code config. |
outputChannel.ts |
Lazy singleton OutputChannel for cleanup reports and LM error logging. |
Key Design Decisions¶
Two-tier Write Locks¶
Every write to .memory/*.md goes through two stacked locks:
-
Outer — cross-process advisory lockfile (
crossProcessLock.ts):withCrossProcessLock(memoryDir, fn)creates.memory/.lockwith an exclusivefs.open('wx')— an atomic OS primitive that works on POSIX and NTFS. Only one process can hold this file at a time. All other processes spin (with linear back-off, max 20 retries) until the holder removes it. Stale locks (dead PID or age > 10 s) are automatically removed, including on extension startup viaclearStaleLockOnStartup(). -
Inner — per-file in-process promise queue (
markdownStore.ts):withFileLock(filePath, fn)chains aPromise<void>per file path. This serialises concurrent async calls within the same extension host process (e.g.storeMemory+ background cleanup hitting the same category file).
Callers outside markdownStore.ts never interact with either lock directly. The combined wrapper withMemoryWriteLock(filePath, fn) is the only entry point used by appendMemory, upsertMemory, deleteMemory, and migrateFiles.
See ADR 0018 for the full decision record.
Centralised LM Access¶
lm.ts is the only module that calls vscode.lm.selectChatModels or sendRequest. All other modules call resolveModel() and sendLmRequest(). This keeps fallback logic, timeout handling, cancellation token forwarding, and error logging in one place.
Two-tier Deduplication¶
- Write-time (fast) — Jaccard similarity against existing entries.
SKIP_THRESHOLD=0.8skips near-identical content.UPDATE_THRESHOLD=0.6replaces similar entries. - Write-time (slow, optional) — LLM redundancy check as a second pass for entries without a matching slug.
- Cleanup (separate) —
findSimilarClusters()uses a lower threshold (0.3 Jaccard) to catch semantically related entries the write-time thresholds missed. These thresholds are intentionally separate constants.
On-demand Memory Injection¶
Rather than injecting the full memory store into copilot-instructions.md, the extension injects a reference table pointing the agent to specific .memory/*.md files. The agent fetches only the categories it needs for a given query. This keeps the instructions file small regardless of how much memory accumulates.
Fail-open LM Calls¶
sendLmRequest always returns string | null. Callers treat null as "proceed without LM assistance." The extension degrades gracefully if Copilot is unavailable, has no models, or the LM call times out. Errors are logged to the output channel.
VS Code API Dependency¶
This extension uses:
vscode.lm.registerTool— to registerstoreMemoryandqueryMemoryas LM toolsvscode.lm.selectChatModels— to select a Copilot model for redundancy checks and gap analysisvscode.lm.onDidChangeChatModels— to refresh the status bar when available models change
These APIs require VS Code 1.99+. The extension publishes to Open VSX; the memory tools function in any editor that implements the VS Code LM Tool API. A separate MCP-based implementation for additional editors is planned (see ADR 0017).
Future: JetBrains Support¶
The storage layer (dedup.ts, scoring.ts, search.ts, markdownStore.ts) and the .memory/*.md file format are editor-agnostic. A future JetBrains plugin would need to:
- Rewrite the VS Code-specific shell (
extension.ts,lm.ts,memoryPanel.ts,memoryTreeView.ts,statusBar.ts) in Kotlin using the IntelliJ Platform SDK. - Extract the storage layer into a shared
core/package (plain TypeScript/Node or a ported Kotlin library). - Implement the same
.memory/*.mdfile format spec (documented in api-reference.md).
See ADR 0016 for the full decision record.