[{"data":1,"prerenderedAt":2278},["ShallowReactive",2],{"article-holographic-memory-potato-vps":3},{"id":4,"title":5,"body":6,"date":2252,"description":2253,"extension":2254,"meta":2255,"navigation":359,"path":2256,"readingTime":2257,"seo":2258,"stem":2259,"tags":2260,"__hash__":2277},"articles\u002Fblog\u002Fholographic-memory-potato-vps.en.md","Holographic Memory for an AI Agent on a Potato VPS",{"type":7,"value":8,"toc":2223},"minimark",[9,13,17,20,28,33,36,116,127,130,134,144,147,151,156,159,163,166,170,173,177,180,184,187,280,283,286,297,301,685,692,696,762,765,769,772,933,939,945,948,952,955,1339,1346,1374,1377,1381,1384,1483,1486,1490,1493,1577,1580,1584,1587,1641,1644,1651,1655,1658,1823,1826,1830,1834,1837,1843,1847,1854,1893,1896,2069,2073,2076,2080,2085,2089,2095,2187,2191,2194,2198,2201,2204,2207,2210,2219],[10,11,5],"h1",{"id":12},"holographic-memory-for-an-ai-agent-on-a-potato-vps",[14,15,16],"p",{},"I have a so-called Potato VPS — a cheap server with minimal RAM. It runs an AI agent — Hermes — that needs to remember context between sessions: facts about users, project configurations, preferences, decisions. Not just \"write to a file and grep it,\" but semantic search that understands paraphrasing and multilingualism.",[14,18,19],{},"The problem: ChromaDB consumes 400 MB just at startup. Pinecone is SaaS, and I want local. FAISS lacks keyword indexing. I need a hybrid: full-text search + vector semantics + compositional algebra. All of this on a modest server, including the embedding model itself.",[14,21,22,23,27],{},"The solution is the ",[24,25,26],"code",{},"holographic-memory"," plugin — four search strategies in a single SQLite file. Here's how it works and why.",[29,30,32],"h2",{"id":31},"architecture-four-strategies-one-query","Architecture: Four Strategies, One Query",[14,34,35],{},"Hybrid scoring isn't \"vector search with FTS fallback\" — it's four independent channels whose results are combined with weights:",[37,38,39,58],"table",{},[40,41,42],"thead",{},[43,44,45,49,52,55],"tr",{},[46,47,48],"th",{},"Strategy",[46,50,51],{},"Weight",[46,53,54],{},"What it does",[46,56,57],{},"Technology",[59,60,61,76,90,103],"tbody",{},[43,62,63,67,70,73],{},[64,65,66],"td",{},"FTS5",[64,68,69],{},"0.3",[64,71,72],{},"Keyword candidates (BM25)",[64,74,75],{},"SQLite FTS5",[43,77,78,81,84,87],{},[64,79,80],{},"Jaccard",[64,82,83],{},"0.2",[64,85,86],{},"Token intersection",[64,88,89],{},"Python sets",[43,91,92,95,97,100],{},[64,93,94],{},"HRR",[64,96,83],{},[64,98,99],{},"Compositional algebra (probe\u002Frelated\u002Freason)",[64,101,102],{},"SHA-256 phase vectors, 1024d",[43,104,105,108,110,113],{},[64,106,107],{},"Semantic",[64,109,69],{},[64,111,112],{},"Semantic similarity",[64,114,115],{},"fastembed MiniLM-L12-v2, 384d",[14,117,118,119,122,123,126],{},"Final score: ",[24,120,121],{},"relevance × trust_score × temporal_decay",", where ",[24,124,125],{},"relevance = fts×0.3 + jaccard×0.2 + hrr×0.2 + semantic×0.3",".",[14,128,129],{},"Why four when you could get by with just vector search? Because vector search performs poorly on short, precise queries (\"nginx deployment order\"), while FTS5 doesn't understand paraphrasing (\"how to roll out nginx to prod\"). HRR provides algebraic operations — probing by entity, finding connections between facts, multi-entity JOINs. Jaccard is a cheap noise filter.",[29,131,133],{"id":132},"search-pipeline","Search Pipeline",[135,136,141],"pre",{"className":137,"code":139,"language":140},[138],"language-text","1. FTS5 MATCH → limit×3 candidates (AND semantics: all terms required)\n2. If FTS5 returns empty + semantic available → _semantic_candidates() (full cosine scan)\n3. Pre-compute query embedding (~46 ms)\n4. Reranking: relevance = fts×0.3 + jaccard×0.2 + hrr×0.2 + semantic×0.3\n5. Final: score = relevance × trust × temporal_decay\n","text",[24,142,139],{"__ignoreMap":143},"",[14,145,146],{},"Key point: if FTS5 returns 0 (query is in Russian while facts were recorded in English, or it's a paraphrase), the pipeline automatically falls back to pure semantic search. Semantic isn't a luxury — it's a load-bearing component for multilingual support.",[29,148,150],{"id":149},"why-not-off-the-shelf-solutions","Why Not Off-the-Shelf Solutions",[152,153,155],"h3",{"id":154},"chromadb","ChromaDB",[14,157,158],{},"Two processes (Chroma + Hermes), ~800 MB before the agent has even remembered anything. On a Potato VPS, that's half the RAM. Plus Chroma pulls in HNSW, which builds its index in memory. For facts (hundreds, maybe thousands of records) — it's using a cannon to kill a mosquito.",[152,160,162],{"id":161},"pinecone","Pinecone",[14,164,165],{},"SaaS. Requires an API key, internet access, and trusting a third party with your agent's data. For a hobby project on a cheap VPS — overkill and vendor lock-in.",[152,167,169],{"id":168},"faiss","FAISS",[14,171,172],{},"Excellent library for vector search, but no full-text indexing. You'd have to bolt FTS5 on top separately. And if you're already using SQLite for both text and vectors — why bother with FAISS?",[152,174,176],{"id":175},"custom-solution","Custom Solution",[14,178,179],{},"SQLite + FTS5 + WAL is already in Python's stdlib (sqlite3). Add fastembed for embeddings and numpy for HRR algebra. One database file, one process, zero infrastructure.",[29,181,183],{"id":182},"model-selection-mpnet-vs-minilm","Model Selection: mpnet vs MiniLM",[14,185,186],{},"I tested two multilingual fastembed models:",[37,188,189,201],{},[40,190,191],{},[43,192,193,195,198],{},[46,194],{},[46,196,197],{},"mpnet-base-v2",[46,199,200],{},"MiniLM-L12-v2",[59,202,203,214,225,236,247,258,269],{},[43,204,205,208,211],{},[64,206,207],{},"Dimensions",[64,209,210],{},"768",[64,212,213],{},"384",[43,215,216,219,222],{},[64,217,218],{},"RSS (loaded)",[64,220,221],{},"1440 MB",[64,223,224],{},"680 MB",[43,226,227,230,233],{},[64,228,229],{},"RSS (residual after unload)",[64,231,232],{},"693 MB",[64,234,235],{},"481 MB",[43,237,238,241,244],{},[64,239,240],{},"Embedding time",[64,242,243],{},"65 ms",[64,245,246],{},"46 ms",[43,248,249,252,255],{},[64,250,251],{},"Reload time after unload",[64,253,254],{},"20–25 s",[64,256,257],{},"1.33 s",[43,259,260,263,266],{},[64,261,262],{},"sim(\"compact format\" ↔ \"concise messages\")",[64,264,265],{},"0.625",[64,267,268],{},"0.511",[43,270,271,274,277],{},[64,272,273],{},"sim(\"compact format\" ↔ \"weather for a walk\")",[64,275,276],{},"0.225",[64,278,279],{},"0.008",[14,281,282],{},"MiniLM: 680 MB RSS, 481 MB residual. mpnet: 1440 MB RSS, 693 MB residual. On a Potato VPS, mpnet doesn't fit — after loading the model + Hermes + the OS, only ~300 MB remains, and the OOM killer comes knocking.",[14,284,285],{},"MiniLM scores 0.511 for semantically similar phrases and 0.008 for unrelated ones — sufficient separation. Not ideal (mpnet is ~20% better), but functional.",[14,287,288,292,293,296],{},[289,290,291],"strong",{},"Residual"," refers to ONNX Runtime, which doesn't release memory pools even after ",[24,294,295],{},"del model + gc.collect()",". 481 MB is the price of a single fastembed invocation during the process lifetime. Hence the strategy: lazy-load on first request, keep in memory until shutdown.",[29,298,300],{"id":299},"lazy-loading-and-model-lifecycle","Lazy-Loading and Model Lifecycle",[135,302,306],{"className":303,"code":304,"language":305,"meta":143,"style":143},"language-python shiki shiki-themes github-light catppuccin-mocha","_model = None\n_available: Optional[bool] = None\n\ndef is_available() -> bool:\n    \"\"\"Check without loading the model.\"\"\"\n    if _available is not None:\n        return _available\n    try:\n        import fastembed\n        _available = True\n    except ImportError:\n        _available = False\n    return _available\n\ndef _get_model():\n    \"\"\"Lazy-load on first embed_text() call.\"\"\"\n    global _model\n    if _model is None:\n        from fastembed import TextEmbedding\n        _model = TextEmbedding(\"sentence-transformers\u002Fparaphrase-multilingual-MiniLM-L12-v2\")\n    return _model\n\ndef embed_text(text: str) -> np.ndarray:\n    model = _get_model()\n    vec = list(model.embed([text]))[0]\n    return vec \u002F np.linalg.norm(vec)  # normalize → unit vector\n","python",[24,307,308,325,354,361,384,391,411,420,428,437,448,460,470,478,483,494,500,509,523,538,559,566,571,603,616,651],{"__ignoreMap":143},[309,310,313,317,321],"span",{"class":311,"line":312},"line",1,[309,314,316],{"class":315},"slTIY","_model ",[309,318,320],{"class":319},"s_Q3D","=",[309,322,324],{"class":323},"sNSVI"," None\n",[309,326,328,331,335,339,342,346,349,352],{"class":311,"line":327},2,[309,329,330],{"class":315},"_available",[309,332,334],{"class":333},"s_QEy",":",[309,336,338],{"class":337},"sO2U0"," Optional",[309,340,341],{"class":333},"[",[309,343,345],{"class":344},"smIoM","bool",[309,347,348],{"class":333},"]",[309,350,351],{"class":319}," =",[309,353,324],{"class":323},[309,355,357],{"class":311,"line":356},3,[309,358,360],{"emptyLinePlaceholder":359},true,"\n",[309,362,364,368,372,375,378,381],{"class":311,"line":363},4,[309,365,367],{"class":366},"saXKZ","def",[309,369,371],{"class":370},"siMrf"," is_available",[309,373,374],{"class":333},"()",[309,376,377],{"class":333}," ->",[309,379,380],{"class":344}," bool",[309,382,383],{"class":333},":\n",[309,385,387],{"class":311,"line":386},5,[309,388,390],{"class":389},"sG7gF","    \"\"\"Check without loading the model.\"\"\"\n",[309,392,394,397,400,403,406,409],{"class":311,"line":393},6,[309,395,396],{"class":366},"    if",[309,398,399],{"class":315}," _available ",[309,401,402],{"class":366},"is",[309,404,405],{"class":366}," not",[309,407,408],{"class":323}," None",[309,410,383],{"class":333},[309,412,414,417],{"class":311,"line":413},7,[309,415,416],{"class":366},"        return",[309,418,419],{"class":315}," _available\n",[309,421,423,426],{"class":311,"line":422},8,[309,424,425],{"class":366},"    try",[309,427,383],{"class":333},[309,429,431,434],{"class":311,"line":430},9,[309,432,433],{"class":366},"        import",[309,435,436],{"class":315}," fastembed\n",[309,438,440,443,445],{"class":311,"line":439},10,[309,441,442],{"class":315},"        _available ",[309,444,320],{"class":319},[309,446,447],{"class":323}," True\n",[309,449,451,454,458],{"class":311,"line":450},11,[309,452,453],{"class":366},"    except",[309,455,457],{"class":456},"sPY-v"," ImportError",[309,459,383],{"class":333},[309,461,463,465,467],{"class":311,"line":462},12,[309,464,442],{"class":315},[309,466,320],{"class":319},[309,468,469],{"class":323}," False\n",[309,471,473,476],{"class":311,"line":472},13,[309,474,475],{"class":366},"    return",[309,477,419],{"class":315},[309,479,481],{"class":311,"line":480},14,[309,482,360],{"emptyLinePlaceholder":359},[309,484,486,488,491],{"class":311,"line":485},15,[309,487,367],{"class":366},[309,489,490],{"class":370}," _get_model",[309,492,493],{"class":333},"():\n",[309,495,497],{"class":311,"line":496},16,[309,498,499],{"class":389},"    \"\"\"Lazy-load on first embed_text() call.\"\"\"\n",[309,501,503,506],{"class":311,"line":502},17,[309,504,505],{"class":366},"    global",[309,507,508],{"class":315}," _model\n",[309,510,512,514,517,519,521],{"class":311,"line":511},18,[309,513,396],{"class":366},[309,515,516],{"class":315}," _model ",[309,518,402],{"class":366},[309,520,408],{"class":323},[309,522,383],{"class":333},[309,524,526,529,532,535],{"class":311,"line":525},19,[309,527,528],{"class":366},"        from",[309,530,531],{"class":315}," fastembed ",[309,533,534],{"class":366},"import",[309,536,537],{"class":315}," TextEmbedding\n",[309,539,541,544,546,550,553,556],{"class":311,"line":540},20,[309,542,543],{"class":315},"        _model ",[309,545,320],{"class":319},[309,547,549],{"class":548},"sPNDc"," TextEmbedding",[309,551,552],{"class":333},"(",[309,554,555],{"class":389},"\"sentence-transformers\u002Fparaphrase-multilingual-MiniLM-L12-v2\"",[309,557,558],{"class":333},")\n",[309,560,562,564],{"class":311,"line":561},21,[309,563,475],{"class":366},[309,565,508],{"class":315},[309,567,569],{"class":311,"line":568},22,[309,570,360],{"emptyLinePlaceholder":359},[309,572,574,576,579,581,583,585,588,591,593,596,598,601],{"class":311,"line":573},23,[309,575,367],{"class":366},[309,577,578],{"class":370}," embed_text",[309,580,552],{"class":333},[309,582,140],{"class":337},[309,584,334],{"class":333},[309,586,587],{"class":344}," str",[309,589,590],{"class":333},")",[309,592,377],{"class":333},[309,594,595],{"class":315}," np",[309,597,126],{"class":333},[309,599,600],{"class":315},"ndarray",[309,602,383],{"class":333},[309,604,606,609,611,613],{"class":311,"line":605},24,[309,607,608],{"class":315},"    model ",[309,610,320],{"class":319},[309,612,490],{"class":548},[309,614,615],{"class":333},"()\n",[309,617,619,622,624,627,629,632,634,637,640,642,645,648],{"class":311,"line":618},25,[309,620,621],{"class":315},"    vec ",[309,623,320],{"class":319},[309,625,626],{"class":344}," list",[309,628,552],{"class":333},[309,630,631],{"class":315},"model",[309,633,126],{"class":333},[309,635,636],{"class":548},"embed",[309,638,639],{"class":333},"([",[309,641,140],{"class":315},[309,643,644],{"class":333},"]))[",[309,646,647],{"class":323},"0",[309,649,650],{"class":333},"]\n",[309,652,654,656,659,662,664,666,669,671,674,676,679,681],{"class":311,"line":653},26,[309,655,475],{"class":366},[309,657,658],{"class":315}," vec ",[309,660,661],{"class":319},"\u002F",[309,663,595],{"class":315},[309,665,126],{"class":333},[309,667,668],{"class":315},"linalg",[309,670,126],{"class":333},[309,672,673],{"class":548},"norm",[309,675,552],{"class":333},[309,677,678],{"class":315},"vec",[309,680,590],{"class":333},[309,682,684],{"class":683},"skkvY","  # normalize → unit vector\n",[14,686,687,688,691],{},"First ",[24,689,690],{},"embed_text()"," call takes ~1.3 s (loading the ONNX model). All subsequent calls take ~46 ms. The model stays in memory until shutdown.",[152,693,695],{"id":694},"unloading-on-shutdown","Unloading on Shutdown",[135,697,699],{"className":303,"code":698,"language":305,"meta":143,"style":143},"# In the plugin's __init__.py, on shutdown hook:\nimport embedder, gc\n\ndef on_shutdown():\n    embedder._model = None\n    gc.collect()\n    # Frees ~200 MB (model weights), but ONNX residual (481 MB) remains\n",[24,700,701,706,719,723,732,745,757],{"__ignoreMap":143},[309,702,703],{"class":311,"line":312},[309,704,705],{"class":683},"# In the plugin's __init__.py, on shutdown hook:\n",[309,707,708,710,713,716],{"class":311,"line":327},[309,709,534],{"class":366},[309,711,712],{"class":315}," embedder",[309,714,715],{"class":333},",",[309,717,718],{"class":315}," gc\n",[309,720,721],{"class":311,"line":356},[309,722,360],{"emptyLinePlaceholder":359},[309,724,725,727,730],{"class":311,"line":363},[309,726,367],{"class":366},[309,728,729],{"class":370}," on_shutdown",[309,731,493],{"class":333},[309,733,734,737,739,741,743],{"class":311,"line":386},[309,735,736],{"class":315},"    embedder",[309,738,126],{"class":333},[309,740,316],{"class":315},[309,742,320],{"class":319},[309,744,324],{"class":323},[309,746,747,750,752,755],{"class":311,"line":393},[309,748,749],{"class":315},"    gc",[309,751,126],{"class":333},[309,753,754],{"class":548},"collect",[309,756,615],{"class":333},[309,758,759],{"class":311,"line":413},[309,760,761],{"class":683},"    # Frees ~200 MB (model weights), but ONNX residual (481 MB) remains\n",[14,763,764],{},"In practice: after shutdown, RSS drops from ~1.1 GB to ~620 MB. ONNX Runtime doesn't give back memory — this is a known quirk. 620 MB is a workable baseline for a Potato VPS.",[29,766,768],{"id":767},"storage-two-vectors-per-fact","Storage: Two Vectors Per Fact",[14,770,771],{},"Each fact in SQLite stores two vectors:",[135,773,777],{"className":774,"code":775,"language":776,"meta":143,"style":143},"language-sql shiki shiki-themes github-light catppuccin-mocha","CREATE TABLE facts (\n    fact_id INTEGER PRIMARY KEY,\n    content TEXT NOT NULL,\n    category TEXT DEFAULT 'general',\n    tags TEXT DEFAULT '',\n    trust_score REAL DEFAULT 0.5,\n    retrieval_count INTEGER DEFAULT 0,\n    helpful_count INTEGER DEFAULT 0,\n    hrr_vector BLOB,        -- 1024 × float64 = 8192 bytes\n    semantic_vector BLOB,   -- 384 × float32 = 1536 bytes\n    created_at TEXT,\n    updated_at TEXT\n);\n","sql",[24,778,779,793,807,820,835,849,869,882,895,903,911,920,928],{"__ignoreMap":143},[309,780,781,784,787,790],{"class":311,"line":312},[309,782,783],{"class":366},"CREATE",[309,785,786],{"class":366}," TABLE",[309,788,789],{"class":370}," facts",[309,791,792],{"class":315}," (\n",[309,794,795,798,801,804],{"class":311,"line":327},[309,796,797],{"class":315},"    fact_id ",[309,799,800],{"class":366},"INTEGER",[309,802,803],{"class":366}," PRIMARY KEY",[309,805,806],{"class":315},",\n",[309,808,809,812,815,818],{"class":311,"line":356},[309,810,811],{"class":315},"    content ",[309,813,814],{"class":366},"TEXT",[309,816,817],{"class":366}," NOT NULL",[309,819,806],{"class":315},[309,821,822,825,827,830,833],{"class":311,"line":363},[309,823,824],{"class":315},"    category ",[309,826,814],{"class":366},[309,828,829],{"class":366}," DEFAULT",[309,831,832],{"class":389}," 'general'",[309,834,806],{"class":315},[309,836,837,840,842,844,847],{"class":311,"line":386},[309,838,839],{"class":315},"    tags ",[309,841,814],{"class":366},[309,843,829],{"class":366},[309,845,846],{"class":389}," ''",[309,848,806],{"class":315},[309,850,851,854,857,859,862,864,867],{"class":311,"line":393},[309,852,853],{"class":315},"    trust_score ",[309,855,856],{"class":366},"REAL",[309,858,829],{"class":366},[309,860,861],{"class":323}," 0",[309,863,126],{"class":315},[309,865,866],{"class":323},"5",[309,868,806],{"class":315},[309,870,871,874,876,878,880],{"class":311,"line":413},[309,872,873],{"class":315},"    retrieval_count ",[309,875,800],{"class":366},[309,877,829],{"class":366},[309,879,861],{"class":323},[309,881,806],{"class":315},[309,883,884,887,889,891,893],{"class":311,"line":422},[309,885,886],{"class":315},"    helpful_count ",[309,888,800],{"class":366},[309,890,829],{"class":366},[309,892,861],{"class":323},[309,894,806],{"class":315},[309,896,897,900],{"class":311,"line":430},[309,898,899],{"class":315},"    hrr_vector BLOB,        ",[309,901,902],{"class":683},"-- 1024 × float64 = 8192 bytes\n",[309,904,905,908],{"class":311,"line":439},[309,906,907],{"class":315},"    semantic_vector BLOB,   ",[309,909,910],{"class":683},"-- 384 × float32 = 1536 bytes\n",[309,912,913,916,918],{"class":311,"line":450},[309,914,915],{"class":315},"    created_at ",[309,917,814],{"class":366},[309,919,806],{"class":315},[309,921,922,925],{"class":311,"line":462},[309,923,924],{"class":315},"    updated_at ",[309,926,927],{"class":366},"TEXT\n",[309,929,930],{"class":311,"line":472},[309,931,932],{"class":315},");\n",[14,934,935,938],{},[289,936,937],{},"HRR vector"," (8 KB per fact) — SHA-256 phase encoding. Content tokens → atom bundle → bind with ROLE_CONTENT and ROLE_ENTITY. Used for algebraic operations: probe (\"all facts about X\"), related (\"what's connected to X\"), reason (\"what do X, Y, Z have in common\"). Runs on numpy, no model needed.",[14,940,941,944],{},[289,942,943],{},"Semantic vector"," (1.5 KB per fact) — fastembed output. Normalized unit vector for cosine similarity. Used in hybrid scoring.",[14,946,947],{},"Total: ~9.7 KB per fact. For 1000 facts — ~10 MB. Negligible.",[29,949,951],{"id":950},"hrr-compositional-algebra-without-neural-networks","HRR: Compositional Algebra Without Neural Networks",[14,953,954],{},"HRR (Holographic Reduced Representations) is a way to encode structure into a fixed-size vector. Instead of training — SHA-256 hashes of tokens converted into phase vectors.",[135,956,958],{"className":303,"code":957,"language":305,"meta":143,"style":143},"def encode_atom(token: str, dim: int = 1024) -> np.ndarray:\n    \"\"\"Token → unit vector in phase space.\"\"\"\n    h = hashlib.sha256(token.encode()).digest()\n    rng = np.frombuffer(h, dtype=np.uint8).astype(np.float64)\n    # Phase code: cos + j*sin → unit vector in complex space\n    phases = rng[:dim] \u002F 255.0 * 2 * np.pi\n    return np.cos(phases) + 1j * np.sin(phases)\n\ndef bind(a, b):\n    \"\"\"Binding: circular convolution in frequency domain.\"\"\"\n    return np.fft.ifft(np.fft.fft(a) * np.fft.fft(b))\n\ndef bundle(vectors):\n    \"\"\"Bundling: element-wise sum + normalization.\"\"\"\n    result = np.sum(vectors, axis=0)\n    return result \u002F np.linalg.norm(result)\n",[24,959,960,1003,1008,1040,1092,1097,1136,1178,1182,1202,1207,1261,1265,1279,1284,1313],{"__ignoreMap":143},[309,961,962,964,967,969,972,974,976,978,981,983,986,988,991,993,995,997,999,1001],{"class":311,"line":312},[309,963,367],{"class":366},[309,965,966],{"class":370}," encode_atom",[309,968,552],{"class":333},[309,970,971],{"class":337},"token",[309,973,334],{"class":333},[309,975,587],{"class":344},[309,977,715],{"class":333},[309,979,980],{"class":337}," dim",[309,982,334],{"class":333},[309,984,985],{"class":344}," int",[309,987,351],{"class":319},[309,989,990],{"class":323}," 1024",[309,992,590],{"class":333},[309,994,377],{"class":333},[309,996,595],{"class":315},[309,998,126],{"class":333},[309,1000,600],{"class":315},[309,1002,383],{"class":333},[309,1004,1005],{"class":311,"line":327},[309,1006,1007],{"class":389},"    \"\"\"Token → unit vector in phase space.\"\"\"\n",[309,1009,1010,1013,1015,1018,1020,1023,1025,1027,1029,1032,1035,1038],{"class":311,"line":356},[309,1011,1012],{"class":315},"    h ",[309,1014,320],{"class":319},[309,1016,1017],{"class":315}," hashlib",[309,1019,126],{"class":333},[309,1021,1022],{"class":548},"sha256",[309,1024,552],{"class":333},[309,1026,971],{"class":315},[309,1028,126],{"class":333},[309,1030,1031],{"class":548},"encode",[309,1033,1034],{"class":333},"()).",[309,1036,1037],{"class":548},"digest",[309,1039,615],{"class":333},[309,1041,1042,1045,1047,1049,1051,1054,1056,1059,1061,1065,1067,1070,1072,1075,1078,1081,1083,1085,1087,1090],{"class":311,"line":363},[309,1043,1044],{"class":315},"    rng ",[309,1046,320],{"class":319},[309,1048,595],{"class":315},[309,1050,126],{"class":333},[309,1052,1053],{"class":548},"frombuffer",[309,1055,552],{"class":333},[309,1057,1058],{"class":315},"h",[309,1060,715],{"class":333},[309,1062,1064],{"class":1063},"s-dMd"," dtype",[309,1066,320],{"class":319},[309,1068,1069],{"class":315},"np",[309,1071,126],{"class":333},[309,1073,1074],{"class":315},"uint8",[309,1076,1077],{"class":333},").",[309,1079,1080],{"class":548},"astype",[309,1082,552],{"class":333},[309,1084,1069],{"class":315},[309,1086,126],{"class":333},[309,1088,1089],{"class":315},"float64",[309,1091,558],{"class":333},[309,1093,1094],{"class":311,"line":386},[309,1095,1096],{"class":683},"    # Phase code: cos + j*sin → unit vector in complex space\n",[309,1098,1099,1102,1104,1107,1110,1113,1115,1118,1121,1124,1127,1129,1131,1133],{"class":311,"line":393},[309,1100,1101],{"class":315},"    phases ",[309,1103,320],{"class":319},[309,1105,1106],{"class":337}," rng",[309,1108,1109],{"class":333},"[:",[309,1111,1112],{"class":337},"dim",[309,1114,348],{"class":333},[309,1116,1117],{"class":319}," \u002F",[309,1119,1120],{"class":323}," 255.0",[309,1122,1123],{"class":319}," *",[309,1125,1126],{"class":323}," 2",[309,1128,1123],{"class":319},[309,1130,595],{"class":315},[309,1132,126],{"class":333},[309,1134,1135],{"class":315},"pi\n",[309,1137,1138,1140,1142,1144,1147,1149,1152,1154,1157,1160,1163,1165,1167,1169,1172,1174,1176],{"class":311,"line":413},[309,1139,475],{"class":366},[309,1141,595],{"class":315},[309,1143,126],{"class":333},[309,1145,1146],{"class":548},"cos",[309,1148,552],{"class":333},[309,1150,1151],{"class":315},"phases",[309,1153,590],{"class":333},[309,1155,1156],{"class":319}," +",[309,1158,1159],{"class":323}," 1",[309,1161,1162],{"class":366},"j",[309,1164,1123],{"class":319},[309,1166,595],{"class":315},[309,1168,126],{"class":333},[309,1170,1171],{"class":548},"sin",[309,1173,552],{"class":333},[309,1175,1151],{"class":315},[309,1177,558],{"class":333},[309,1179,1180],{"class":311,"line":422},[309,1181,360],{"emptyLinePlaceholder":359},[309,1183,1184,1186,1189,1191,1194,1196,1199],{"class":311,"line":430},[309,1185,367],{"class":366},[309,1187,1188],{"class":370}," bind",[309,1190,552],{"class":333},[309,1192,1193],{"class":337},"a",[309,1195,715],{"class":333},[309,1197,1198],{"class":337}," b",[309,1200,1201],{"class":333},"):\n",[309,1203,1204],{"class":311,"line":439},[309,1205,1206],{"class":389},"    \"\"\"Binding: circular convolution in frequency domain.\"\"\"\n",[309,1208,1209,1211,1213,1215,1218,1220,1223,1225,1227,1229,1231,1233,1235,1237,1239,1241,1243,1245,1247,1249,1251,1253,1255,1258],{"class":311,"line":450},[309,1210,475],{"class":366},[309,1212,595],{"class":315},[309,1214,126],{"class":333},[309,1216,1217],{"class":315},"fft",[309,1219,126],{"class":333},[309,1221,1222],{"class":548},"ifft",[309,1224,552],{"class":333},[309,1226,1069],{"class":315},[309,1228,126],{"class":333},[309,1230,1217],{"class":315},[309,1232,126],{"class":333},[309,1234,1217],{"class":548},[309,1236,552],{"class":333},[309,1238,1193],{"class":315},[309,1240,590],{"class":333},[309,1242,1123],{"class":319},[309,1244,595],{"class":315},[309,1246,126],{"class":333},[309,1248,1217],{"class":315},[309,1250,126],{"class":333},[309,1252,1217],{"class":548},[309,1254,552],{"class":333},[309,1256,1257],{"class":315},"b",[309,1259,1260],{"class":333},"))\n",[309,1262,1263],{"class":311,"line":462},[309,1264,360],{"emptyLinePlaceholder":359},[309,1266,1267,1269,1272,1274,1277],{"class":311,"line":472},[309,1268,367],{"class":366},[309,1270,1271],{"class":370}," bundle",[309,1273,552],{"class":333},[309,1275,1276],{"class":337},"vectors",[309,1278,1201],{"class":333},[309,1280,1281],{"class":311,"line":480},[309,1282,1283],{"class":389},"    \"\"\"Bundling: element-wise sum + normalization.\"\"\"\n",[309,1285,1286,1289,1291,1293,1295,1298,1300,1302,1304,1307,1309,1311],{"class":311,"line":485},[309,1287,1288],{"class":315},"    result ",[309,1290,320],{"class":319},[309,1292,595],{"class":315},[309,1294,126],{"class":333},[309,1296,1297],{"class":548},"sum",[309,1299,552],{"class":333},[309,1301,1276],{"class":315},[309,1303,715],{"class":333},[309,1305,1306],{"class":1063}," axis",[309,1308,320],{"class":319},[309,1310,647],{"class":323},[309,1312,558],{"class":333},[309,1314,1315,1317,1320,1322,1324,1326,1328,1330,1332,1334,1337],{"class":311,"line":496},[309,1316,475],{"class":366},[309,1318,1319],{"class":315}," result ",[309,1321,661],{"class":319},[309,1323,595],{"class":315},[309,1325,126],{"class":333},[309,1327,668],{"class":315},[309,1329,126],{"class":333},[309,1331,673],{"class":548},[309,1333,552],{"class":333},[309,1335,1336],{"class":315},"result",[309,1338,558],{"class":333},[14,1340,1341,1342,1345],{},"Why bother when you have fastembed? Because HRR supports ",[289,1343,1344],{},"algebraic operations"," that vector models can't do:",[1347,1348,1349,1356,1362],"ul",{},[1350,1351,1352,1355],"li",{},[289,1353,1354],{},"probe(entity)"," — \"give me all facts bound to entity X.\" Bind\u002Funbind with the entity atom.",[1350,1357,1358,1361],{},[289,1359,1360],{},"related(entity)"," — \"what structurally neighbors X.\" Via HRR similarity.",[1350,1363,1364,1370,1371,126],{},[289,1365,1366,1367,590],{},"reason(",[309,1368,1369],{},"e1, e2, e3"," — \"what do multiple entities have in common.\" Multi-entity JOIN, ",[24,1372,1373],{},"min(scores)",[14,1375,1376],{},"The semantic model gives \"similarity in meaning,\" HRR gives \"connectedness by structure.\" These are different axes.",[29,1378,1380],{"id":1379},"jaccard-a-cheap-filter","Jaccard: A Cheap Filter",[14,1382,1383],{},"Jaccard is the intersection of query and fact tokens divided by their union. Cost: O(n) over tokens, zero memory allocations. Works as a coarse filter: if the query and fact share no tokens — apply a penalty.",[135,1385,1387],{"className":303,"code":1386,"language":305,"meta":143,"style":143},"def jaccard(query_tokens: set, fact_tokens: set) -> float:\n    if not query_tokens or not fact_tokens:\n        return 0.0\n    return len(query_tokens & fact_tokens) \u002F len(query_tokens | fact_tokens)\n",[24,1388,1389,1424,1442,1449],{"__ignoreMap":143},[309,1390,1391,1393,1396,1398,1401,1403,1406,1408,1411,1413,1415,1417,1419,1422],{"class":311,"line":312},[309,1392,367],{"class":366},[309,1394,1395],{"class":370}," jaccard",[309,1397,552],{"class":333},[309,1399,1400],{"class":337},"query_tokens",[309,1402,334],{"class":333},[309,1404,1405],{"class":344}," set",[309,1407,715],{"class":333},[309,1409,1410],{"class":337}," fact_tokens",[309,1412,334],{"class":333},[309,1414,1405],{"class":344},[309,1416,590],{"class":333},[309,1418,377],{"class":333},[309,1420,1421],{"class":344}," float",[309,1423,383],{"class":333},[309,1425,1426,1428,1430,1433,1436,1438,1440],{"class":311,"line":327},[309,1427,396],{"class":366},[309,1429,405],{"class":366},[309,1431,1432],{"class":315}," query_tokens ",[309,1434,1435],{"class":366},"or",[309,1437,405],{"class":366},[309,1439,1410],{"class":315},[309,1441,383],{"class":333},[309,1443,1444,1446],{"class":311,"line":356},[309,1445,416],{"class":366},[309,1447,1448],{"class":323}," 0.0\n",[309,1450,1451,1453,1456,1458,1461,1464,1466,1468,1470,1472,1474,1476,1479,1481],{"class":311,"line":363},[309,1452,475],{"class":366},[309,1454,1455],{"class":456}," len",[309,1457,552],{"class":333},[309,1459,1460],{"class":315},"query_tokens ",[309,1462,1463],{"class":319},"&",[309,1465,1410],{"class":315},[309,1467,590],{"class":333},[309,1469,1117],{"class":319},[309,1471,1455],{"class":456},[309,1473,552],{"class":333},[309,1475,1460],{"class":315},[309,1477,1478],{"class":319},"|",[309,1480,1410],{"class":315},[309,1482,558],{"class":333},[14,1484,1485],{},"Weight of 0.2 — not the primary channel, but cuts through noise. In practice: query \"deploy nginx\" and fact \"DNS configuration\" get jaccard=0, and rightly so.",[29,1487,1489],{"id":1488},"fts5-full-text-indexing","FTS5: Full-Text Indexing",[14,1491,1492],{},"SQLite FTS5 is built-in full-text indexing. AND semantics: all query words must appear in the fact. Strict, but predictable.",[135,1494,1496],{"className":774,"code":1495,"language":776,"meta":143,"style":143},"CREATE VIRTUAL TABLE facts_fts USING fts5(content, content=facts, content_rowid=fact_id);\n\n-- Search:\nSELECT fact_id, rank FROM facts_fts WHERE facts_fts MATCH 'deploy nginx'\nORDER BY rank LIMIT 30;\n",[24,1497,1498,1527,1531,1536,1560],{"__ignoreMap":143},[309,1499,1500,1502,1505,1508,1511,1514,1517,1519,1522,1524],{"class":311,"line":312},[309,1501,783],{"class":366},[309,1503,1504],{"class":315}," VIRTUAL ",[309,1506,1507],{"class":366},"TABLE",[309,1509,1510],{"class":315}," facts_fts ",[309,1512,1513],{"class":366},"USING",[309,1515,1516],{"class":315}," fts5(content, content",[309,1518,320],{"class":319},[309,1520,1521],{"class":315},"facts, content_rowid",[309,1523,320],{"class":319},[309,1525,1526],{"class":315},"fact_id);\n",[309,1528,1529],{"class":311,"line":327},[309,1530,360],{"emptyLinePlaceholder":359},[309,1532,1533],{"class":311,"line":356},[309,1534,1535],{"class":683},"-- Search:\n",[309,1537,1538,1541,1544,1547,1549,1552,1554,1557],{"class":311,"line":363},[309,1539,1540],{"class":366},"SELECT",[309,1542,1543],{"class":315}," fact_id, rank ",[309,1545,1546],{"class":366},"FROM",[309,1548,1510],{"class":315},[309,1550,1551],{"class":366},"WHERE",[309,1553,1510],{"class":315},[309,1555,1556],{"class":366},"MATCH",[309,1558,1559],{"class":389}," 'deploy nginx'\n",[309,1561,1562,1565,1568,1571,1574],{"class":311,"line":386},[309,1563,1564],{"class":366},"ORDER BY",[309,1566,1567],{"class":315}," rank ",[309,1569,1570],{"class":366},"LIMIT",[309,1572,1573],{"class":323}," 30",[309,1575,1576],{"class":315},";\n",[14,1578,1579],{},"The problem with FTS5: it doesn't understand paraphrasing. \"How to roll out nginx to prod\" won't match \"nginx deployment via CI\u002FCD.\" That's why falling back to semantic search when FTS5 returns empty is critical.",[29,1581,1583],{"id":1582},"resource-usage-on-a-potato-vps","Resource Usage on a Potato VPS",[14,1585,1586],{},"Typical memory footprint with the agent running:",[37,1588,1589,1599],{},[40,1590,1591],{},[43,1592,1593,1596],{},[46,1594,1595],{},"Component",[46,1597,1598],{},"RSS",[59,1600,1601,1609,1617,1625,1633],{},[43,1602,1603,1606],{},[64,1604,1605],{},"Hermes core (Python)",[64,1607,1608],{},"~380 MB",[43,1610,1611,1614],{},[64,1612,1613],{},"fastembed (loaded)",[64,1615,1616],{},"+300 MB",[43,1618,1619,1622],{},[64,1620,1621],{},"ONNX Runtime residual",[64,1623,1624],{},"(included above)",[43,1626,1627,1630],{},[64,1628,1629],{},"SQLite + FTS5 index",[64,1631,1632],{},"~5 MB",[43,1634,1635,1638],{},[64,1636,1637],{},"Total",[64,1639,1640],{},"~680 MB",[14,1642,1643],{},"Plenty left for the OS, swap, and other processes. Comfortable.",[14,1645,1646,1647,1650],{},"On shutdown, the model is unloaded and RSS drops to ~620 MB. ONNX residual isn't freed — that's the cost of a single ",[24,1648,1649],{},"import fastembed"," per process lifetime.",[29,1652,1654],{"id":1653},"weight-auto-redistribution","Weight Auto-Redistribution",[14,1656,1657],{},"If fastembed is unavailable (not installed, or numpy missing), weights are redistributed automatically:",[135,1659,1661],{"className":303,"code":1660,"language":305,"meta":143,"style":143},"def _redistribute_weights(self):\n    if not embedder.is_available():\n        # Semantic unavailable: 0.3 → FTS +0.15, Jaccard +0.1, HRR +0.05\n        self.fts_weight = 0.45\n        self.jaccard_weight = 0.30\n        self.hrr_weight = 0.25\n        self.semantic_weight = 0.0\n    elif not _HAS_NUMPY:\n        # HRR unavailable: 0.2 → FTS +0.1, Semantic +0.1\n        self.fts_weight = 0.40\n        self.jaccard_weight = 0.20\n        self.hrr_weight = 0.0\n        self.semantic_weight = 0.40\n",[24,1662,1663,1678,1693,1698,1714,1728,1742,1755,1768,1773,1786,1799,1811],{"__ignoreMap":143},[309,1664,1665,1667,1670,1672,1676],{"class":311,"line":312},[309,1666,367],{"class":366},[309,1668,1669],{"class":370}," _redistribute_weights",[309,1671,552],{"class":333},[309,1673,1675],{"class":1674},"s7pD5","self",[309,1677,1201],{"class":333},[309,1679,1680,1682,1684,1686,1688,1691],{"class":311,"line":327},[309,1681,396],{"class":366},[309,1683,405],{"class":366},[309,1685,712],{"class":315},[309,1687,126],{"class":333},[309,1689,1690],{"class":548},"is_available",[309,1692,493],{"class":333},[309,1694,1695],{"class":311,"line":356},[309,1696,1697],{"class":683},"        # Semantic unavailable: 0.3 → FTS +0.15, Jaccard +0.1, HRR +0.05\n",[309,1699,1700,1704,1706,1709,1711],{"class":311,"line":363},[309,1701,1703],{"class":1702},"sG_o1","        self",[309,1705,126],{"class":333},[309,1707,1708],{"class":315},"fts_weight ",[309,1710,320],{"class":319},[309,1712,1713],{"class":323}," 0.45\n",[309,1715,1716,1718,1720,1723,1725],{"class":311,"line":386},[309,1717,1703],{"class":1702},[309,1719,126],{"class":333},[309,1721,1722],{"class":315},"jaccard_weight ",[309,1724,320],{"class":319},[309,1726,1727],{"class":323}," 0.30\n",[309,1729,1730,1732,1734,1737,1739],{"class":311,"line":393},[309,1731,1703],{"class":1702},[309,1733,126],{"class":333},[309,1735,1736],{"class":315},"hrr_weight ",[309,1738,320],{"class":319},[309,1740,1741],{"class":323}," 0.25\n",[309,1743,1744,1746,1748,1751,1753],{"class":311,"line":413},[309,1745,1703],{"class":1702},[309,1747,126],{"class":333},[309,1749,1750],{"class":315},"semantic_weight ",[309,1752,320],{"class":319},[309,1754,1448],{"class":323},[309,1756,1757,1760,1762,1766],{"class":311,"line":422},[309,1758,1759],{"class":366},"    elif",[309,1761,405],{"class":366},[309,1763,1765],{"class":1764},"sbIxs"," _HAS_NUMPY",[309,1767,383],{"class":333},[309,1769,1770],{"class":311,"line":430},[309,1771,1772],{"class":683},"        # HRR unavailable: 0.2 → FTS +0.1, Semantic +0.1\n",[309,1774,1775,1777,1779,1781,1783],{"class":311,"line":439},[309,1776,1703],{"class":1702},[309,1778,126],{"class":333},[309,1780,1708],{"class":315},[309,1782,320],{"class":319},[309,1784,1785],{"class":323}," 0.40\n",[309,1787,1788,1790,1792,1794,1796],{"class":311,"line":450},[309,1789,1703],{"class":1702},[309,1791,126],{"class":333},[309,1793,1722],{"class":315},[309,1795,320],{"class":319},[309,1797,1798],{"class":323}," 0.20\n",[309,1800,1801,1803,1805,1807,1809],{"class":311,"line":462},[309,1802,1703],{"class":1702},[309,1804,126],{"class":333},[309,1806,1736],{"class":315},[309,1808,320],{"class":319},[309,1810,1448],{"class":323},[309,1812,1813,1815,1817,1819,1821],{"class":311,"line":472},[309,1814,1703],{"class":1702},[309,1816,126],{"class":333},[309,1818,1750],{"class":315},[309,1820,320],{"class":319},[309,1822,1785],{"class":323},[14,1824,1825],{},"Graceful degradation: the system works without embeddings (FTS + Jaccard) and without HRR (FTS + Jaccard + Semantic). But the full quartet is optimal.",[29,1827,1829],{"id":1828},"practical-pitfalls","Practical Pitfalls",[152,1831,1833],{"id":1832},"_1-fts5-and-is-too-strict","1. FTS5 AND Is Too Strict",[14,1835,1836],{},"Query \"compact message format\" requires all three words present. If the fact was recorded as \"concise responses\" — FTS5 stays silent. Semantic saves the day, but only if the model is loaded.",[14,1838,1839,1842],{},[289,1840,1841],{},"Solution:"," Don't rely on FTS5 as the sole channel. Always keep semantic enabled.",[152,1844,1846],{"id":1845},"_2-missing-semantic-vectors-after-update","2. Missing Semantic Vectors After Update",[14,1848,1849,1850,1853],{},"The agent was updated, but the old process is still running. New facts are written without ",[24,1851,1852],{},"semantic_vector",". Symptom: Russian queries return empty results.",[135,1855,1857],{"className":774,"code":1856,"language":776,"meta":143,"style":143},"SELECT COUNT(*) FROM facts WHERE semantic_vector IS NULL;\n",[24,1858,1859],{"__ignoreMap":143},[309,1860,1861,1863,1867,1869,1872,1875,1877,1880,1882,1885,1888,1891],{"class":311,"line":312},[309,1862,1540],{"class":366},[309,1864,1866],{"class":1865},"sRaXx"," COUNT",[309,1868,552],{"class":315},[309,1870,1871],{"class":319},"*",[309,1873,1874],{"class":315},") ",[309,1876,1546],{"class":366},[309,1878,1879],{"class":315}," facts ",[309,1881,1551],{"class":366},[309,1883,1884],{"class":315}," semantic_vector ",[309,1886,1887],{"class":366},"IS",[309,1889,1890],{"class":366}," NULL",[309,1892,1576],{"class":315},[14,1894,1895],{},"If > 0 — run a backfill:",[135,1897,1899],{"className":303,"code":1898,"language":305,"meta":143,"style":143},"import embedder, sqlite3\n\nconn = sqlite3.connect(db_path)  # path to your database\nrows = conn.execute('SELECT fact_id, content FROM facts WHERE semantic_vector IS NULL').fetchall()\n\nfor fid, content in rows:\n    vec = embedder.embed_text(content)\n    conn.execute('UPDATE facts SET semantic_vector = ? WHERE fact_id = ?',\n                 (embedder.vector_to_bytes(vec), fid))\n\nconn.commit()\n",[24,1900,1901,1912,1916,1941,1968,1972,1993,2013,2029,2053,2057],{"__ignoreMap":143},[309,1902,1903,1905,1907,1909],{"class":311,"line":312},[309,1904,534],{"class":366},[309,1906,712],{"class":315},[309,1908,715],{"class":333},[309,1910,1911],{"class":315}," sqlite3\n",[309,1913,1914],{"class":311,"line":327},[309,1915,360],{"emptyLinePlaceholder":359},[309,1917,1918,1921,1923,1926,1928,1931,1933,1936,1938],{"class":311,"line":356},[309,1919,1920],{"class":315},"conn ",[309,1922,320],{"class":319},[309,1924,1925],{"class":315}," sqlite3",[309,1927,126],{"class":333},[309,1929,1930],{"class":548},"connect",[309,1932,552],{"class":333},[309,1934,1935],{"class":315},"db_path",[309,1937,590],{"class":333},[309,1939,1940],{"class":683},"  # path to your database\n",[309,1942,1943,1946,1948,1951,1953,1956,1958,1961,1963,1966],{"class":311,"line":363},[309,1944,1945],{"class":315},"rows ",[309,1947,320],{"class":319},[309,1949,1950],{"class":315}," conn",[309,1952,126],{"class":333},[309,1954,1955],{"class":548},"execute",[309,1957,552],{"class":333},[309,1959,1960],{"class":389},"'SELECT fact_id, content FROM facts WHERE semantic_vector IS NULL'",[309,1962,1077],{"class":333},[309,1964,1965],{"class":548},"fetchall",[309,1967,615],{"class":333},[309,1969,1970],{"class":311,"line":386},[309,1971,360],{"emptyLinePlaceholder":359},[309,1973,1974,1977,1980,1982,1985,1988,1991],{"class":311,"line":393},[309,1975,1976],{"class":366},"for",[309,1978,1979],{"class":315}," fid",[309,1981,715],{"class":333},[309,1983,1984],{"class":315}," content ",[309,1986,1987],{"class":366},"in",[309,1989,1990],{"class":315}," rows",[309,1992,383],{"class":333},[309,1994,1995,1997,1999,2001,2003,2006,2008,2011],{"class":311,"line":413},[309,1996,621],{"class":315},[309,1998,320],{"class":319},[309,2000,712],{"class":315},[309,2002,126],{"class":333},[309,2004,2005],{"class":548},"embed_text",[309,2007,552],{"class":333},[309,2009,2010],{"class":315},"content",[309,2012,558],{"class":333},[309,2014,2015,2018,2020,2022,2024,2027],{"class":311,"line":422},[309,2016,2017],{"class":315},"    conn",[309,2019,126],{"class":333},[309,2021,1955],{"class":548},[309,2023,552],{"class":333},[309,2025,2026],{"class":389},"'UPDATE facts SET semantic_vector = ? WHERE fact_id = ?'",[309,2028,806],{"class":333},[309,2030,2031,2034,2037,2039,2042,2044,2046,2049,2051],{"class":311,"line":430},[309,2032,2033],{"class":333},"                 (",[309,2035,2036],{"class":315},"embedder",[309,2038,126],{"class":333},[309,2040,2041],{"class":548},"vector_to_bytes",[309,2043,552],{"class":333},[309,2045,678],{"class":315},[309,2047,2048],{"class":333},"),",[309,2050,1979],{"class":315},[309,2052,1260],{"class":333},[309,2054,2055],{"class":311,"line":439},[309,2056,360],{"emptyLinePlaceholder":359},[309,2058,2059,2062,2064,2067],{"class":311,"line":450},[309,2060,2061],{"class":315},"conn",[309,2063,126],{"class":333},[309,2065,2066],{"class":548},"commit",[309,2068,615],{"class":333},[152,2070,2072],{"id":2071},"_3-fastembed-pooling-change","3. fastembed Pooling Change",[14,2074,2075],{},"Version 0.8.0+ changes pooling from CLS to mean. Old vectors are incompatible with new ones. Solution: full re-embedding of all facts after upgrading fastembed.",[152,2077,2079],{"id":2078},"_4-onnx-memory-leak-its-not-a-leak","4. ONNX Memory Leak (It's Not a Leak)",[14,2081,2082,2084],{},[24,2083,295],{}," frees the weights but not the ONNX Runtime pools. This isn't a leak — it's how ONNX works. 481 MB residual is normal. Don't try to \"fix\" it.",[152,2086,2088],{"id":2087},"_5-rss-used-memory","5. RSS ≠ Used Memory",[14,2090,2091,2094],{},[24,2092,2093],{},"ru_maxrss"," shows the peak, not current consumption. For accurate measurement:",[135,2096,2098],{"className":303,"code":2097,"language":305,"meta":143,"style":143},"def current_rss_mb():\n    with open('\u002Fproc\u002Fself\u002Fstatm') as f:\n        pages = int(f.read().split()[1])\n    return pages * 4096 \u002F 1024 \u002F 1024\n",[24,2099,2100,2109,2132,2166],{"__ignoreMap":143},[309,2101,2102,2104,2107],{"class":311,"line":312},[309,2103,367],{"class":366},[309,2105,2106],{"class":370}," current_rss_mb",[309,2108,493],{"class":333},[309,2110,2111,2114,2117,2119,2122,2124,2127,2130],{"class":311,"line":327},[309,2112,2113],{"class":366},"    with",[309,2115,2116],{"class":456}," open",[309,2118,552],{"class":333},[309,2120,2121],{"class":389},"'\u002Fproc\u002Fself\u002Fstatm'",[309,2123,590],{"class":333},[309,2125,2126],{"class":366}," as",[309,2128,2129],{"class":315}," f",[309,2131,383],{"class":333},[309,2133,2134,2137,2139,2141,2143,2146,2148,2151,2154,2157,2160,2163],{"class":311,"line":356},[309,2135,2136],{"class":315},"        pages ",[309,2138,320],{"class":319},[309,2140,985],{"class":344},[309,2142,552],{"class":333},[309,2144,2145],{"class":315},"f",[309,2147,126],{"class":333},[309,2149,2150],{"class":548},"read",[309,2152,2153],{"class":333},"().",[309,2155,2156],{"class":548},"split",[309,2158,2159],{"class":333},"()[",[309,2161,2162],{"class":323},"1",[309,2164,2165],{"class":333},"])\n",[309,2167,2168,2170,2173,2175,2178,2180,2182,2184],{"class":311,"line":363},[309,2169,475],{"class":366},[309,2171,2172],{"class":315}," pages ",[309,2174,1871],{"class":319},[309,2176,2177],{"class":323}," 4096",[309,2179,1117],{"class":319},[309,2181,990],{"class":323},[309,2183,1117],{"class":319},[309,2185,2186],{"class":323}," 1024\n",[29,2188,2190],{"id":2189},"web-interface-for-viewing-facts","Web Interface for Viewing Facts",[14,2192,2193],{},"For debugging, I built a standalone htmx app on stdlib's http.server. Dark theme, monospace font, FTS5 search, inline editing, feedback buttons, color-coded category badges. Zero dependencies — just Python stdlib. Launches with a single command, listens on a local port.",[29,2195,2197],{"id":2196},"summary","Summary",[14,2199,2200],{},"Four search strategies in a single SQLite file. 680 MB RSS with the model loaded. Lazy-loading, graceful degradation, unloading on shutdown. No external services, no docker-compose, no Pinecone API keys.",[14,2202,2203],{},"For a hobby project on a Potato VPS — this is the only sensible option. Not because it's \"better than ChromaDB,\" but because ChromaDB doesn't fit in modest RAM, and Pinecone is someone else's computer.",[14,2205,2206],{},"A custom SQLite plugin means control. Control over memory, over indexing, over the model lifecycle. And when the OOM killer comes knocking at 3 AM — you know exactly who's to blame and what to do.",[2208,2209],"hr",{},[14,2211,2212],{},[2213,2214,2215,2216,2218],"em",{},"The ",[24,2217,26],{}," plugin is part of the Hermes Agent project.",[2220,2221,2222],"style",{},"html pre.shiki code .slTIY, html code.shiki .slTIY{--shiki-light:#24292E;--shiki-dark:#CDD6F4}html pre.shiki code .s_Q3D, html code.shiki .s_Q3D{--shiki-light:#D73A49;--shiki-dark:#94E2D5}html pre.shiki code .sNSVI, html code.shiki .sNSVI{--shiki-light:#005CC5;--shiki-dark:#FAB387}html pre.shiki code .s_QEy, html code.shiki .s_QEy{--shiki-light:#24292E;--shiki-dark:#9399B2}html pre.shiki code .sO2U0, html code.shiki .sO2U0{--shiki-light:#24292E;--shiki-light-font-style:inherit;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic}html pre.shiki code .smIoM, html code.shiki .smIoM{--shiki-light:#005CC5;--shiki-light-font-style:inherit;--shiki-dark:#CBA6F7;--shiki-dark-font-style:italic}html pre.shiki code .saXKZ, html code.shiki .saXKZ{--shiki-light:#D73A49;--shiki-dark:#CBA6F7}html pre.shiki code .siMrf, html code.shiki .siMrf{--shiki-light:#6F42C1;--shiki-light-font-style:inherit;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic}html pre.shiki code .sG7gF, html code.shiki .sG7gF{--shiki-light:#032F62;--shiki-dark:#A6E3A1}html pre.shiki code .sPY-v, html code.shiki .sPY-v{--shiki-light:#005CC5;--shiki-light-font-style:inherit;--shiki-dark:#FAB387;--shiki-dark-font-style:italic}html pre.shiki code .sPNDc, html code.shiki .sPNDc{--shiki-light:#24292E;--shiki-dark:#89B4FA}html pre.shiki code .skkvY, html code.shiki .skkvY{--shiki-light:#6A737D;--shiki-light-font-style:inherit;--shiki-dark:#9399B2;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-dMd, html code.shiki .s-dMd{--shiki-light:#E36209;--shiki-light-font-style:inherit;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic}html pre.shiki code .s7pD5, html code.shiki .s7pD5{--shiki-light:#24292E;--shiki-light-font-style:inherit;--shiki-dark:#F38BA8;--shiki-dark-font-style:italic}html pre.shiki code .sG_o1, html code.shiki .sG_o1{--shiki-light:#005CC5;--shiki-light-font-style:inherit;--shiki-dark:#F38BA8;--shiki-dark-font-style:italic}html pre.shiki code .sbIxs, html code.shiki .sbIxs{--shiki-light:#005CC5;--shiki-dark:#CDD6F4}html pre.shiki code .sRaXx, html code.shiki .sRaXx{--shiki-light:#005CC5;--shiki-light-font-style:inherit;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic}",{"title":143,"searchDepth":327,"depth":327,"links":2224},[2225,2226,2227,2233,2234,2237,2238,2239,2240,2241,2242,2243,2250,2251],{"id":31,"depth":327,"text":32},{"id":132,"depth":327,"text":133},{"id":149,"depth":327,"text":150,"children":2228},[2229,2230,2231,2232],{"id":154,"depth":356,"text":155},{"id":161,"depth":356,"text":162},{"id":168,"depth":356,"text":169},{"id":175,"depth":356,"text":176},{"id":182,"depth":327,"text":183},{"id":299,"depth":327,"text":300,"children":2235},[2236],{"id":694,"depth":356,"text":695},{"id":767,"depth":327,"text":768},{"id":950,"depth":327,"text":951},{"id":1379,"depth":327,"text":1380},{"id":1488,"depth":327,"text":1489},{"id":1582,"depth":327,"text":1583},{"id":1653,"depth":327,"text":1654},{"id":1828,"depth":327,"text":1829,"children":2244},[2245,2246,2247,2248,2249],{"id":1832,"depth":356,"text":1833},{"id":1845,"depth":356,"text":1846},{"id":2071,"depth":356,"text":2072},{"id":2078,"depth":356,"text":2079},{"id":2087,"depth":356,"text":2088},{"id":2189,"depth":327,"text":2190},{"id":2196,"depth":327,"text":2197},"2026-05-24","How to run hybrid search (FTS5 + Jaccard + HRR + fastembed MiniLM-L12-v2) on a cheap VPS. Lazy-loaded model, ~680 MB RSS, unloaded on shutdown. Why not ChromaDB or Pinecone — but a custom SQLite plugin instead.","md",{},"\u002Fblog\u002Fholographic-memory-potato-vps.en",null,{"title":5,"description":2253},"blog\u002Fholographic-memory-potato-vps.en",[2261,2262,2263,2264,2265,2266,2267,2268,2269,2270,2271,2272,2273,2274,2275,2276],"ai-memory","vector-search","embeddings","sqlite","fts5","fastembed","semantic-search","self-hosted","potato-vps","rag","hybrid-search","nlp","mini-lm","hrr","jaccard","performance-optimization","Vbu0LOt9VqRHbKaDUMfhallbA3hPwesw-SEk99eUP1I",1780777846248]