[{"data":1,"prerenderedAt":11160},["ShallowReactive",2],{"articles-all-i18n":3},[4,687,1236,3475,5359,7086,8618,9975],{"id":5,"title":6,"body":7,"date":671,"description":672,"extension":673,"meta":674,"navigation":345,"path":675,"readingTime":676,"seo":677,"stem":678,"tags":679,"__hash__":686},"articles\u002Fblog\u002Fcloudflare-hermes-integration.md","Hermes + Cloudflare: автоматизация DNS и деплоя из терминала",{"type":8,"value":9,"toc":655},"minimark",[10,14,18,21,26,29,48,51,55,65,91,94,129,132,136,141,144,194,197,235,242,246,288,292,316,320,323,391,394,461,471,475,482,488,491,495,498,503,506,520,523,527,530,553,556,560,566,572,621,627,641,645,648,651],[11,12,6],"h1",{"id":13},"hermes-cloudflare-автоматизация-dns-и-деплоя-из-терминала",[15,16,17],"p",{},"У меня десятки субдоменов: блог, лендинги, API, нотификационные сервисы. Управлять всем этим через Cloudflare Dashboard — больно. Каждый раз логиниться, искать нужную запись, кликать, ждать...",[15,19,20],{},"Решение — дать это дело AI-агенту. Hermes Agent умеет работать с Cloudflare через CLI и API. Вот как я настроил этот pipeline.",[22,23,25],"h2",{"id":24},"зачем-агенту-cloudflare","Зачем агенту Cloudflare?",[15,27,28],{},"Типичный сценарий: я ставлю новый сервис на VPS и хочу, чтобы он был доступен по HTTPS на своём субдомене. Раньше это было:",[30,31,32,36,39,42,45],"ol",{},[33,34,35],"li",{},"Зайти в Cloudflare Dashboard",[33,37,38],{},"Создать DNS-запись",[33,40,41],{},"Дождаться пропагации",[33,43,44],{},"Проверить SSL",[33,46,47],{},"Настроить Caddy\u002Fnginx",[15,49,50],{},"Теперь я говорю Hermes: «Подними ntfy на субдомене notify и проксируй через Cloudflare». Агент делает всё сам.",[22,52,54],{"id":53},"инструмент-flarectl","Инструмент: flarectl",[15,56,57,64],{},[58,59,63],"a",{"href":60,"rel":61},"https:\u002F\u002Fgithub.com\u002Fcloudflare\u002Fcloudflare-go\u002Ftree\u002Fmain\u002Fcmd\u002Fflarectl",[62],"nofollow","flarectl"," — официальная CLI-утилита от Cloudflare. Установка:",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-bash shiki shiki-themes github-light catppuccin-mocha","go install github.com\u002Fcloudflare\u002Fcloudflare-go\u002Fcmd\u002Fflarectl@latest\n","bash","",[73,74,75],"code",{"__ignoreMap":71},[76,77,80,84,88],"span",{"class":78,"line":79},"line",1,[76,81,83],{"class":82},"siMrf","go",[76,85,87],{"class":86},"sG7gF"," install",[76,89,90],{"class":86}," github.com\u002Fcloudflare\u002Fcloudflare-go\u002Fcmd\u002Fflarectl@latest\n",[15,92,93],{},"Авторизация через переменные окружения:",[66,95,97],{"className":68,"code":96,"language":70,"meta":71,"style":71},"export CF_API_TOKEN=your-api-token\nexport CF_ACCOUNT_ID=your-account-id\n",[73,98,99,116],{"__ignoreMap":71},[76,100,101,105,109,113],{"class":78,"line":79},[76,102,104],{"class":103},"saXKZ","export",[76,106,108],{"class":107},"slTIY"," CF_API_TOKEN",[76,110,112],{"class":111},"s_Q3D","=",[76,114,115],{"class":107},"your-api-token\n",[76,117,119,121,124,126],{"class":78,"line":118},2,[76,120,104],{"class":103},[76,122,123],{"class":107}," CF_ACCOUNT_ID",[76,125,112],{"class":111},[76,127,128],{"class":107},"your-account-id\n",[15,130,131],{},"Токен создаётся в Dashboard → My Profile → API Tokens. Нужны permissions: Zone:DNS:Edit, Zone:Zone:Read.",[22,133,135],{"id":134},"базовые-операции","Базовые операции",[137,138,140],"h3",{"id":139},"создание-dns-записи","Создание DNS-записи",[15,142,143],{},"A-запись (субдомен → IP сервера):",[66,145,147],{"className":68,"code":146,"language":70,"meta":71,"style":71},"flarectl dns create --zone example.com \\\n  --type A --name myservice --content 203.0.113.1 --proxied\n",[73,148,149,170],{"__ignoreMap":71},[76,150,151,153,156,159,163,166],{"class":78,"line":79},[76,152,63],{"class":82},[76,154,155],{"class":86}," dns",[76,157,158],{"class":86}," create",[76,160,162],{"class":161},"soLUO"," --zone",[76,164,165],{"class":86}," example.com",[76,167,169],{"class":168},"s_VIv"," \\\n",[76,171,172,175,178,181,184,187,191],{"class":78,"line":118},[76,173,174],{"class":161},"  --type",[76,176,177],{"class":86}," A",[76,179,180],{"class":161}," --name",[76,182,183],{"class":86}," myservice",[76,185,186],{"class":161}," --content",[76,188,190],{"class":189},"sNSVI"," 203.0.113.1",[76,192,193],{"class":161}," --proxied\n",[15,195,196],{},"CNAME (субдомен → Cloudflare Pages проект):",[66,198,200],{"className":68,"code":199,"language":70,"meta":71,"style":71},"flarectl dns create --zone example.com \\\n  --type CNAME --name blog --content my-project.pages.dev --proxied\n",[73,201,202,216],{"__ignoreMap":71},[76,203,204,206,208,210,212,214],{"class":78,"line":79},[76,205,63],{"class":82},[76,207,155],{"class":86},[76,209,158],{"class":86},[76,211,162],{"class":161},[76,213,165],{"class":86},[76,215,169],{"class":168},[76,217,218,220,223,225,228,230,233],{"class":78,"line":118},[76,219,174],{"class":161},[76,221,222],{"class":86}," CNAME",[76,224,180],{"class":161},[76,226,227],{"class":86}," blog",[76,229,186],{"class":161},[76,231,232],{"class":86}," my-project.pages.dev",[76,234,193],{"class":161},[15,236,237,238,241],{},"Флаг ",[73,239,240],{},"--proxied"," включает оранжевый облако — Cloudflare кеширует, защищает от DDoS, terminates SSL.",[137,243,245],{"id":244},"обновление-записей","Обновление записей",[66,247,249],{"className":68,"code":248,"language":70,"meta":71,"style":71},"flarectl dns update --zone example.com \\\n  --id RECORD_ID --type A --name myservice --content 203.0.113.2\n",[73,250,251,266],{"__ignoreMap":71},[76,252,253,255,257,260,262,264],{"class":78,"line":79},[76,254,63],{"class":82},[76,256,155],{"class":86},[76,258,259],{"class":86}," update",[76,261,162],{"class":161},[76,263,165],{"class":86},[76,265,169],{"class":168},[76,267,268,271,274,277,279,281,283,285],{"class":78,"line":118},[76,269,270],{"class":161},"  --id",[76,272,273],{"class":86}," RECORD_ID",[76,275,276],{"class":161}," --type",[76,278,177],{"class":86},[76,280,180],{"class":161},[76,282,183],{"class":86},[76,284,186],{"class":161},[76,286,287],{"class":189}," 203.0.113.2\n",[137,289,291],{"id":290},"удаление","Удаление",[66,293,295],{"className":68,"code":294,"language":70,"meta":71,"style":71},"flarectl dns delete --zone example.com --id RECORD_ID\n",[73,296,297],{"__ignoreMap":71},[76,298,299,301,303,306,308,310,313],{"class":78,"line":79},[76,300,63],{"class":82},[76,302,155],{"class":86},[76,304,305],{"class":86}," delete",[76,307,162],{"class":161},[76,309,165],{"class":86},[76,311,312],{"class":161}," --id",[76,314,315],{"class":86}," RECORD_ID\n",[22,317,319],{"id":318},"cloudflare-pages-деплой-статики","Cloudflare Pages: деплой статики",[15,321,322],{},"Для Nuxt, Astro, Next.js и других SSG\u002FSSR фреймворков Cloudflare Pages — бесплатный хостинг с CDN. Деплой через wrangler CLI:",[66,324,326],{"className":68,"code":325,"language":70,"meta":71,"style":71},"npm install -g wrangler\n\n# Авторизация\nwrangler login\n\n# Деплой из папки dist\u002F\nwrangler pages deploy dist --project-name=my-blog\n",[73,327,328,341,347,354,363,368,374],{"__ignoreMap":71},[76,329,330,333,335,338],{"class":78,"line":79},[76,331,332],{"class":82},"npm",[76,334,87],{"class":86},[76,336,337],{"class":161}," -g",[76,339,340],{"class":86}," wrangler\n",[76,342,343],{"class":78,"line":118},[76,344,346],{"emptyLinePlaceholder":345},true,"\n",[76,348,350],{"class":78,"line":349},3,[76,351,353],{"class":352},"skkvY","# Авторизация\n",[76,355,357,360],{"class":78,"line":356},4,[76,358,359],{"class":82},"wrangler",[76,361,362],{"class":86}," login\n",[76,364,366],{"class":78,"line":365},5,[76,367,346],{"emptyLinePlaceholder":345},[76,369,371],{"class":78,"line":370},6,[76,372,373],{"class":352},"# Деплой из папки dist\u002F\n",[76,375,377,379,382,385,388],{"class":78,"line":376},7,[76,378,359],{"class":82},[76,380,381],{"class":86}," pages",[76,383,384],{"class":86}," deploy",[76,386,387],{"class":86}," dist",[76,389,390],{"class":161}," --project-name=my-blog\n",[15,392,393],{},"Кастомный домен привязывается через API:",[66,395,397],{"className":68,"code":396,"language":70,"meta":71,"style":71},"curl -s -X POST \\\n  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Faccounts\u002F$CF_ACCOUNT_ID\u002Fpages\u002Fprojects\u002Fmy-blog\u002Fdomains\" \\\n  -H \"Authorization: Bearer $CF_API_TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  --data '{\"name\": \"blog.example.com\"}'\n",[73,398,399,415,428,444,453],{"__ignoreMap":71},[76,400,401,404,407,410,413],{"class":78,"line":79},[76,402,403],{"class":82},"curl",[76,405,406],{"class":161}," -s",[76,408,409],{"class":161}," -X",[76,411,412],{"class":86}," POST",[76,414,169],{"class":168},[76,416,417,420,423,426],{"class":78,"line":118},[76,418,419],{"class":86},"  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Faccounts\u002F",[76,421,422],{"class":107},"$CF_ACCOUNT_ID",[76,424,425],{"class":86},"\u002Fpages\u002Fprojects\u002Fmy-blog\u002Fdomains\"",[76,427,169],{"class":168},[76,429,430,433,436,439,442],{"class":78,"line":349},[76,431,432],{"class":161},"  -H",[76,434,435],{"class":86}," \"Authorization: Bearer ",[76,437,438],{"class":107},"$CF_API_TOKEN",[76,440,441],{"class":86},"\"",[76,443,169],{"class":168},[76,445,446,448,451],{"class":78,"line":356},[76,447,432],{"class":161},[76,449,450],{"class":86}," \"Content-Type: application\u002Fjson\"",[76,452,169],{"class":168},[76,454,455,458],{"class":78,"line":365},[76,456,457],{"class":161},"  --data",[76,459,460],{"class":86}," '{\"name\": \"blog.example.com\"}'\n",[462,463,464],"blockquote",{},[15,465,466,467,470],{},"Wrangler v4.94+ может не поддерживать ",[73,468,469],{},"wrangler pages domain add",". Используйте API напрямую.",[22,472,474],{"id":473},"ssl-flexible-vs-full","SSL: Flexible vs Full",[15,476,477,481],{},[478,479,480],"strong",{},"SSL:Flexible"," — Cloudflare terminates SSL на своём edge, до вашего origin идёт plain HTTP. Не нужен сертификат на сервере. Идеально для сервисов, которые не работают с HTTPS нативно.",[15,483,484,487],{},[478,485,486],{},"SSL:Full"," — Cloudflare terminates SSL на edge, но до origin тоже идёт SSL. Нужен сертификат на сервере (можно Cloudflare Origin CA — бесплатный).",[15,489,490],{},"Для self-hosted сервисов на одном VPS обычно хватает Flexible. Но если сервис передаёт чувствительные данные — ставьте Full.",[22,492,494],{"id":493},"автоматизация-через-hermes","Автоматизация через Hermes",[15,496,497],{},"Вот как это работает в реальной жизни. Hermes Agent имеет доступ к терминалу и может выполнять команды. Я говорю:",[462,499,500],{},[15,501,502],{},"«Поставь Grafana на монитор.example.com»",[15,504,505],{},"Агент:",[30,507,508,511,514,517],{},[33,509,510],{},"Проверяет, что Grafana установлена",[33,512,513],{},"Создаёт DNS A-запись через flarectl",[33,515,516],{},"Настраивает Caddy reverse proxy",[33,518,519],{},"Проверяет HTTPS",[15,521,522],{},"Всё в одном диалоге, без переключения в Dashboard.",[137,524,526],{"id":525},"cron-мониторинг","Cron-мониторинг",[15,528,529],{},"Hermes может проверять DNS-записи по расписанию:",[66,531,533],{"className":68,"code":532,"language":70,"meta":71,"style":71},"flarectl dns list --zone example.com --type A\n",[73,534,535],{"__ignoreMap":71},[76,536,537,539,541,544,546,548,550],{"class":78,"line":79},[76,538,63],{"class":82},[76,540,155],{"class":86},[76,542,543],{"class":86}," list",[76,545,162],{"class":161},[76,547,165],{"class":86},[76,549,276],{"class":161},[76,551,552],{"class":86}," A\n",[15,554,555],{},"Если запись изменилась или пропала — агент отправляет уведомление.",[22,557,559],{"id":558},"подводные-камни","Подводные камни",[15,561,562,565],{},[478,563,564],{},"Proxy status."," Если запись не proxied (серое облако), Cloudflare не terminates SSL и не кеширует. Для сервисов, которые должны быть за CDN — всегда proxied.",[15,567,568,571],{},[478,569,570],{},"Cache purge."," После деплоя нового билда Cloudflare может отдавать старый кеш. Очистка:",[66,573,575],{"className":68,"code":574,"language":70,"meta":71,"style":71},"curl -s -X POST \\\n  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F$ZONE_ID\u002Fpurge_cache\" \\\n  -H \"Authorization: Bearer $CF_API_TOKEN\" \\\n  --data '{\"purge_everything\":true}'\n",[73,576,577,589,602,614],{"__ignoreMap":71},[76,578,579,581,583,585,587],{"class":78,"line":79},[76,580,403],{"class":82},[76,582,406],{"class":161},[76,584,409],{"class":161},[76,586,412],{"class":86},[76,588,169],{"class":168},[76,590,591,594,597,600],{"class":78,"line":118},[76,592,593],{"class":86},"  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F",[76,595,596],{"class":107},"$ZONE_ID",[76,598,599],{"class":86},"\u002Fpurge_cache\"",[76,601,169],{"class":168},[76,603,604,606,608,610,612],{"class":78,"line":349},[76,605,432],{"class":161},[76,607,435],{"class":86},[76,609,438],{"class":107},[76,611,441],{"class":86},[76,613,169],{"class":168},[76,615,616,618],{"class":78,"line":356},[76,617,457],{"class":161},[76,619,620],{"class":86}," '{\"purge_everything\":true}'\n",[15,622,623,626],{},[478,624,625],{},"Rate limits."," Cloudflare Free план имеет лимиты на API-вызовы. Для автоматизации это редко проблема, но если агент делает десятки запросов в минуту — может словить 429.",[15,628,629,632,633,636,637,640],{},[478,630,631],{},"DNS пропагация."," После создания записи DNS пропагируется за 1-5 минут. Hermes проверяет через ",[73,634,635],{},"dig"," или ",[73,638,639],{},"flarectl dns list"," перед тем, как считать задачу выполненной.",[22,642,644],{"id":643},"итог","Итог",[15,646,647],{},"Cloudflare Free + Hermes Agent = полная автоматизация инфраструктуры из терминала. Никакого Dashboard, никаких ручных кликов. Агент управляет DNS, деплоит статику, настраивает SSL, мониторит записи.",[15,649,650],{},"Для self-hosted проектов это идеальный стек: бесплатно, надёжно, и всё через CLI.",[652,653,654],"style",{},"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 .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 .saXKZ, html code.shiki .saXKZ{--shiki-light:#D73A49;--shiki-dark:#CBA6F7}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 .soLUO, html code.shiki .soLUO{--shiki-light:#005CC5;--shiki-dark:#A6E3A1}html pre.shiki code .s_VIv, html code.shiki .s_VIv{--shiki-light:#005CC5;--shiki-dark:#F5C2E7}html pre.shiki code .sNSVI, html code.shiki .sNSVI{--shiki-light:#005CC5;--shiki-dark:#FAB387}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}",{"title":71,"searchDepth":118,"depth":118,"links":656},[657,658,659,664,665,666,669,670],{"id":24,"depth":118,"text":25},{"id":53,"depth":118,"text":54},{"id":134,"depth":118,"text":135,"children":660},[661,662,663],{"id":139,"depth":349,"text":140},{"id":244,"depth":349,"text":245},{"id":290,"depth":349,"text":291},{"id":318,"depth":118,"text":319},{"id":473,"depth":118,"text":474},{"id":493,"depth":118,"text":494,"children":667},[668],{"id":525,"depth":349,"text":526},{"id":558,"depth":118,"text":559},{"id":643,"depth":118,"text":644},"2026-05-24","Как AI-агент Hermes управляет DNS-записями, деплоит статику на Cloudflare Pages и настраивает SSL — всё через CLI, без Dashboard.","md",{},"\u002Fblog\u002Fcloudflare-hermes-integration",null,{"title":6,"description":672},"blog\u002Fcloudflare-hermes-integration",[680,681,682,683,684,685],"hermes","cloudflare","dns","devops","automation","cli","zBwXmjqPSypVC4vi_B8eEFZcexsExVqR5xCE6q_VDDA",{"id":688,"title":689,"body":690,"date":671,"description":1229,"extension":673,"meta":1230,"navigation":345,"path":1231,"readingTime":676,"seo":1232,"stem":1233,"tags":1234,"__hash__":1235},"articles\u002Fblog\u002Fcloudflare-hermes-integration.en.md","Hermes + Cloudflare: DNS and Deployment Automation from the Terminal",{"type":8,"value":691,"toc":1213},[692,695,698,701,705,708,725,728,732,738,750,753,777,780,784,788,791,825,828,862,868,872,908,912,932,936,939,990,993,1045,1053,1055,1060,1065,1068,1072,1075,1080,1083,1097,1100,1104,1107,1127,1130,1134,1139,1144,1184,1189,1201,1205,1208,1211],[11,693,689],{"id":694},"hermes-cloudflare-dns-and-deployment-automation-from-the-terminal",[15,696,697],{},"I have dozens of subdomains: blogs, landing pages, APIs, notification services. Managing all of this through the Cloudflare Dashboard is painful. Logging in every time, finding the right record, clicking around, waiting...",[15,699,700],{},"The solution is to hand it off to an AI agent. Hermes Agent can work with Cloudflare via CLI and API. Here's how I set up this pipeline.",[22,702,704],{"id":703},"why-does-an-agent-need-cloudflare","Why Does an Agent Need Cloudflare?",[15,706,707],{},"A typical scenario: I deploy a new service on a VPS and want it accessible over HTTPS on its own subdomain. It used to be:",[30,709,710,713,716,719,722],{},[33,711,712],{},"Log into the Cloudflare Dashboard",[33,714,715],{},"Create a DNS record",[33,717,718],{},"Wait for propagation",[33,720,721],{},"Verify SSL",[33,723,724],{},"Configure Caddy\u002Fnginx",[15,726,727],{},"Now I just tell Hermes: \"Set up ntfy on the notify subdomain and proxy it through Cloudflare.\" The agent handles everything itself.",[22,729,731],{"id":730},"tool-flarectl","Tool: flarectl",[15,733,734,737],{},[58,735,63],{"href":60,"rel":736},[62]," is the official CLI utility from Cloudflare. Installation:",[66,739,740],{"className":68,"code":69,"language":70,"meta":71,"style":71},[73,741,742],{"__ignoreMap":71},[76,743,744,746,748],{"class":78,"line":79},[76,745,83],{"class":82},[76,747,87],{"class":86},[76,749,90],{"class":86},[15,751,752],{},"Authentication via environment variables:",[66,754,755],{"className":68,"code":96,"language":70,"meta":71,"style":71},[73,756,757,767],{"__ignoreMap":71},[76,758,759,761,763,765],{"class":78,"line":79},[76,760,104],{"class":103},[76,762,108],{"class":107},[76,764,112],{"class":111},[76,766,115],{"class":107},[76,768,769,771,773,775],{"class":78,"line":118},[76,770,104],{"class":103},[76,772,123],{"class":107},[76,774,112],{"class":111},[76,776,128],{"class":107},[15,778,779],{},"The token is created in Dashboard → My Profile → API Tokens. Required permissions: Zone:DNS:Edit, Zone:Zone:Read.",[22,781,783],{"id":782},"basic-operations","Basic Operations",[137,785,787],{"id":786},"creating-a-dns-record","Creating a DNS Record",[15,789,790],{},"A record (subdomain → server IP):",[66,792,793],{"className":68,"code":146,"language":70,"meta":71,"style":71},[73,794,795,809],{"__ignoreMap":71},[76,796,797,799,801,803,805,807],{"class":78,"line":79},[76,798,63],{"class":82},[76,800,155],{"class":86},[76,802,158],{"class":86},[76,804,162],{"class":161},[76,806,165],{"class":86},[76,808,169],{"class":168},[76,810,811,813,815,817,819,821,823],{"class":78,"line":118},[76,812,174],{"class":161},[76,814,177],{"class":86},[76,816,180],{"class":161},[76,818,183],{"class":86},[76,820,186],{"class":161},[76,822,190],{"class":189},[76,824,193],{"class":161},[15,826,827],{},"CNAME (subdomain → Cloudflare Pages project):",[66,829,830],{"className":68,"code":199,"language":70,"meta":71,"style":71},[73,831,832,846],{"__ignoreMap":71},[76,833,834,836,838,840,842,844],{"class":78,"line":79},[76,835,63],{"class":82},[76,837,155],{"class":86},[76,839,158],{"class":86},[76,841,162],{"class":161},[76,843,165],{"class":86},[76,845,169],{"class":168},[76,847,848,850,852,854,856,858,860],{"class":78,"line":118},[76,849,174],{"class":161},[76,851,222],{"class":86},[76,853,180],{"class":161},[76,855,227],{"class":86},[76,857,186],{"class":161},[76,859,232],{"class":86},[76,861,193],{"class":161},[15,863,864,865,867],{},"The ",[73,866,240],{}," flag enables the orange cloud — Cloudflare caches, protects against DDoS, and terminates SSL.",[137,869,871],{"id":870},"updating-records","Updating Records",[66,873,874],{"className":68,"code":248,"language":70,"meta":71,"style":71},[73,875,876,890],{"__ignoreMap":71},[76,877,878,880,882,884,886,888],{"class":78,"line":79},[76,879,63],{"class":82},[76,881,155],{"class":86},[76,883,259],{"class":86},[76,885,162],{"class":161},[76,887,165],{"class":86},[76,889,169],{"class":168},[76,891,892,894,896,898,900,902,904,906],{"class":78,"line":118},[76,893,270],{"class":161},[76,895,273],{"class":86},[76,897,276],{"class":161},[76,899,177],{"class":86},[76,901,180],{"class":161},[76,903,183],{"class":86},[76,905,186],{"class":161},[76,907,287],{"class":189},[137,909,911],{"id":910},"deleting","Deleting",[66,913,914],{"className":68,"code":294,"language":70,"meta":71,"style":71},[73,915,916],{"__ignoreMap":71},[76,917,918,920,922,924,926,928,930],{"class":78,"line":79},[76,919,63],{"class":82},[76,921,155],{"class":86},[76,923,305],{"class":86},[76,925,162],{"class":161},[76,927,165],{"class":86},[76,929,312],{"class":161},[76,931,315],{"class":86},[22,933,935],{"id":934},"cloudflare-pages-deploying-static-sites","Cloudflare Pages: Deploying Static Sites",[15,937,938],{},"For Nuxt, Astro, Next.js, and other SSG\u002FSSR frameworks, Cloudflare Pages offers free hosting with a CDN. Deployment via the wrangler CLI:",[66,940,942],{"className":68,"code":941,"language":70,"meta":71,"style":71},"npm install -g wrangler\n\n# Authentication\nwrangler login\n\n# Deploy from dist\u002F folder\nwrangler pages deploy dist --project-name=my-blog\n",[73,943,944,954,958,963,969,973,978],{"__ignoreMap":71},[76,945,946,948,950,952],{"class":78,"line":79},[76,947,332],{"class":82},[76,949,87],{"class":86},[76,951,337],{"class":161},[76,953,340],{"class":86},[76,955,956],{"class":78,"line":118},[76,957,346],{"emptyLinePlaceholder":345},[76,959,960],{"class":78,"line":349},[76,961,962],{"class":352},"# Authentication\n",[76,964,965,967],{"class":78,"line":356},[76,966,359],{"class":82},[76,968,362],{"class":86},[76,970,971],{"class":78,"line":365},[76,972,346],{"emptyLinePlaceholder":345},[76,974,975],{"class":78,"line":370},[76,976,977],{"class":352},"# Deploy from dist\u002F folder\n",[76,979,980,982,984,986,988],{"class":78,"line":376},[76,981,359],{"class":82},[76,983,381],{"class":86},[76,985,384],{"class":86},[76,987,387],{"class":86},[76,989,390],{"class":161},[15,991,992],{},"A custom domain is attached via the API:",[66,994,996],{"className":68,"code":995,"language":70,"meta":71,"style":71},"curl -s -X POST \\\n  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Faccounts\u002F$CF_ACCOUNT_ID\u002Fpages\u002Fprojects\u002Fmy-blog\u002Fdomains\" \\\n  -H \"Authorization: Bearer *** \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  --data '{\"name\": \"blog.example.com\"}'\n",[73,997,998,1010,1020,1030,1040],{"__ignoreMap":71},[76,999,1000,1002,1004,1006,1008],{"class":78,"line":79},[76,1001,403],{"class":82},[76,1003,406],{"class":161},[76,1005,409],{"class":161},[76,1007,412],{"class":86},[76,1009,169],{"class":168},[76,1011,1012,1014,1016,1018],{"class":78,"line":118},[76,1013,419],{"class":86},[76,1015,422],{"class":107},[76,1017,425],{"class":86},[76,1019,169],{"class":168},[76,1021,1022,1024,1027],{"class":78,"line":349},[76,1023,432],{"class":161},[76,1025,1026],{"class":86}," \"Authorization: Bearer *** ",[76,1028,1029],{"class":168},"\\\n",[76,1031,1032,1035,1038],{"class":78,"line":356},[76,1033,1034],{"class":86},"  -H \"Content-Type:",[76,1036,1037],{"class":86}," application\u002Fjson\" ",[76,1039,1029],{"class":168},[76,1041,1042],{"class":78,"line":365},[76,1043,1044],{"class":86},"  --data '{\"name\": \"blog.example.com\"}'\n",[462,1046,1047],{},[15,1048,1049,1050,1052],{},"Wrangler v4.94+ may not support ",[73,1051,469],{},". Use the API directly.",[22,1054,474],{"id":473},[15,1056,1057,1059],{},[478,1058,480],{}," — Cloudflare terminates SSL at its edge; plain HTTP goes to your origin. No certificate needed on the server. Ideal for services that don't natively support HTTPS.",[15,1061,1062,1064],{},[478,1063,486],{}," — Cloudflare terminates SSL at the edge, but also uses SSL to connect to the origin. A certificate is required on the server (you can use a free Cloudflare Origin CA certificate).",[15,1066,1067],{},"For self-hosted services on a single VPS, Flexible is usually sufficient. However, if the service transmits sensitive data, use Full.",[22,1069,1071],{"id":1070},"automation-via-hermes","Automation via Hermes",[15,1073,1074],{},"Here's how it works in real life. Hermes Agent has terminal access and can execute commands. I say:",[462,1076,1077],{},[15,1078,1079],{},"\"Set up Grafana on monitor.example.com\"",[15,1081,1082],{},"The agent:",[30,1084,1085,1088,1091,1094],{},[33,1086,1087],{},"Checks that Grafana is installed",[33,1089,1090],{},"Creates a DNS A record via flarectl",[33,1092,1093],{},"Configures the Caddy reverse proxy",[33,1095,1096],{},"Verifies HTTPS",[15,1098,1099],{},"All in one conversation, without switching to the Dashboard.",[137,1101,1103],{"id":1102},"cron-monitoring","Cron Monitoring",[15,1105,1106],{},"Hermes can check DNS records on a schedule:",[66,1108,1109],{"className":68,"code":532,"language":70,"meta":71,"style":71},[73,1110,1111],{"__ignoreMap":71},[76,1112,1113,1115,1117,1119,1121,1123,1125],{"class":78,"line":79},[76,1114,63],{"class":82},[76,1116,155],{"class":86},[76,1118,543],{"class":86},[76,1120,162],{"class":161},[76,1122,165],{"class":86},[76,1124,276],{"class":161},[76,1126,552],{"class":86},[15,1128,1129],{},"If a record has changed or disappeared, the agent sends a notification.",[22,1131,1133],{"id":1132},"pitfalls","Pitfalls",[15,1135,1136,1138],{},[478,1137,564],{}," If a record is not proxied (grey cloud), Cloudflare won't terminate SSL or cache. For services that should be behind a CDN, always use proxied.",[15,1140,1141,1143],{},[478,1142,570],{}," After deploying a new build, Cloudflare might serve the old cache. To purge:",[66,1145,1147],{"className":68,"code":1146,"language":70,"meta":71,"style":71},"curl -s -X POST \\\n  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F$ZONE_ID\u002Fpurge_cache\" \\\n  -H \"Authorization: Bearer *** \\\n  --data '{\"purge_everything\":true}'\n",[73,1148,1149,1161,1171,1179],{"__ignoreMap":71},[76,1150,1151,1153,1155,1157,1159],{"class":78,"line":79},[76,1152,403],{"class":82},[76,1154,406],{"class":161},[76,1156,409],{"class":161},[76,1158,412],{"class":86},[76,1160,169],{"class":168},[76,1162,1163,1165,1167,1169],{"class":78,"line":118},[76,1164,593],{"class":86},[76,1166,596],{"class":107},[76,1168,599],{"class":86},[76,1170,169],{"class":168},[76,1172,1173,1175,1177],{"class":78,"line":349},[76,1174,432],{"class":161},[76,1176,1026],{"class":86},[76,1178,1029],{"class":168},[76,1180,1181],{"class":78,"line":356},[76,1182,1183],{"class":86},"  --data '{\"purge_everything\":true}'\n",[15,1185,1186,1188],{},[478,1187,625],{}," The Cloudflare Free plan has API call limits. This is rarely an issue for automation, but if the agent makes dozens of requests per minute, it might hit a 429 error.",[15,1190,1191,1194,1195,1197,1198,1200],{},[478,1192,1193],{},"DNS propagation."," After creating a record, DNS propagates within 1-5 minutes. Hermes verifies via ",[73,1196,635],{}," or ",[73,1199,639],{}," before considering the task complete.",[22,1202,1204],{"id":1203},"conclusion","Conclusion",[15,1206,1207],{},"Cloudflare Free + Hermes Agent = full infrastructure automation from the terminal. No Dashboard, no manual clicking. The agent manages DNS, deploys static sites, configures SSL, and monitors records.",[15,1209,1210],{},"For self-hosted projects, this is the ideal stack: free, reliable, and entirely CLI-driven.",[652,1212,654],{},{"title":71,"searchDepth":118,"depth":118,"links":1214},[1215,1216,1217,1222,1223,1224,1227,1228],{"id":703,"depth":118,"text":704},{"id":730,"depth":118,"text":731},{"id":782,"depth":118,"text":783,"children":1218},[1219,1220,1221],{"id":786,"depth":349,"text":787},{"id":870,"depth":349,"text":871},{"id":910,"depth":349,"text":911},{"id":934,"depth":118,"text":935},{"id":473,"depth":118,"text":474},{"id":1070,"depth":118,"text":1071,"children":1225},[1226],{"id":1102,"depth":349,"text":1103},{"id":1132,"depth":118,"text":1133},{"id":1203,"depth":118,"text":1204},"How the Hermes AI agent manages DNS records, deploys static sites to Cloudflare Pages, and configures SSL — all via CLI, without the Dashboard.",{},"\u002Fblog\u002Fcloudflare-hermes-integration.en",{"title":689,"description":1229},"blog\u002Fcloudflare-hermes-integration.en",[680,681,682,683,684,685],"nvPKwgwqsfvRT1G5f-mr-qS-d4WldqKgG6CkTcUxQbQ",{"id":1237,"title":1238,"body":1239,"date":671,"description":3452,"extension":673,"meta":3453,"navigation":345,"path":3454,"readingTime":676,"seo":3455,"stem":3456,"tags":3457,"__hash__":3474},"articles\u002Fblog\u002Fholographic-memory-potato-vps.md","Голографическая память для AI-агента на Potato VPS",{"type":8,"value":1240,"toc":3423},[1241,1244,1247,1250,1257,1261,1264,1344,1355,1358,1362,1370,1373,1377,1381,1384,1388,1391,1395,1398,1402,1405,1409,1412,1505,1508,1511,1521,1525,1889,1896,1900,1966,1969,1973,1976,2137,2143,2149,2152,2156,2159,2542,2549,2576,2579,2583,2586,2685,2688,2692,2695,2779,2782,2786,2789,2843,2846,2853,2857,2860,3025,3028,3032,3036,3039,3045,3049,3056,3095,3098,3271,3275,3278,3282,3287,3291,3297,3389,3393,3396,3399,3402,3405,3408,3411,3420],[11,1242,1238],{"id":1243},"голографическая-память-для-ai-агента-на-potato-vps",[15,1245,1246],{},"У меня есть так называемый Potato VPS — дешёвый сервер с минимумом оперативки. На нём крутится AI-агент — Hermes — который должен помнить контекст между сессиями: факты о пользователях, конфигурации проектов, предпочтения, решения. Не просто «записать в файл и grep'нуть», а смысловой поиск с пониманием перефразировок и мультиязычности.",[15,1248,1249],{},"Проблема: ChromaDB жрёт 400 МБ просто на старте. Pinecone — это SaaS, а я хочу локально. FAISS — без индексации по ключевым словам. Мне нужен гибрид: полнотекстовый поиск + векторная семантика + композиционная алгебра. И всё это на скромном сервере, включая саму модель эмбеддингов.",[15,1251,1252,1253,1256],{},"Решение — плагин ",[73,1254,1255],{},"holographic-memory",", четыре стратегии поиска в одном SQLite-файле. Вот как это устроено и почему.",[22,1258,1260],{"id":1259},"архитектура-четыре-стратегии-один-запрос","Архитектура: четыре стратегии, один запрос",[15,1262,1263],{},"Гибридный скоринг — не «векторный поиск с fallback на FTS», а четыре независимых канала, результаты которых складываются с весами:",[1265,1266,1267,1286],"table",{},[1268,1269,1270],"thead",{},[1271,1272,1273,1277,1280,1283],"tr",{},[1274,1275,1276],"th",{},"Стратегия",[1274,1278,1279],{},"Вес",[1274,1281,1282],{},"Что делает",[1274,1284,1285],{},"Технология",[1287,1288,1289,1304,1318,1331],"tbody",{},[1271,1290,1291,1295,1298,1301],{},[1292,1293,1294],"td",{},"FTS5",[1292,1296,1297],{},"0.3",[1292,1299,1300],{},"Кандидаты по ключевым словам (BM25)",[1292,1302,1303],{},"SQLite FTS5",[1271,1305,1306,1309,1312,1315],{},[1292,1307,1308],{},"Jaccard",[1292,1310,1311],{},"0.2",[1292,1313,1314],{},"Пересечение токенов",[1292,1316,1317],{},"Python sets",[1271,1319,1320,1323,1325,1328],{},[1292,1321,1322],{},"HRR",[1292,1324,1311],{},[1292,1326,1327],{},"Композиционная алгебра (probe\u002Frelated\u002Freason)",[1292,1329,1330],{},"SHA-256 phase vectors, 1024d",[1271,1332,1333,1336,1338,1341],{},[1292,1334,1335],{},"Semantic",[1292,1337,1297],{},[1292,1339,1340],{},"Смысловое сходство",[1292,1342,1343],{},"fastembed MiniLM-L12-v2, 384d",[15,1345,1346,1347,1350,1351,1354],{},"Финальный скор: ",[73,1348,1349],{},"relevance × trust_score × temporal_decay",", где ",[73,1352,1353],{},"relevance = fts×0.3 + jaccard×0.2 + hrr×0.2 + semantic×0.3",".",[15,1356,1357],{},"Зачем четыре, если можно одним векторным поиском обойтись? Потому что векторный поиск плохо работает на коротких точных запросах («порядок деплоя nginx»), а FTS5 не понимает перефразировок («как выкатить nginx на прод»). HRR даёт алгебраические операции — probe по сущности, связь между фактами, мульти-сущностные JOIN'ы. Jaccard — дешёвый фильтр мусора.",[22,1359,1361],{"id":1360},"запуск-поиска-пайплайн","Запуск поиска: пайплайн",[66,1363,1368],{"className":1364,"code":1366,"language":1367},[1365],"language-text","1. FTS5 MATCH → limit×3 кандидатов (AND-семантика: все термы обязательны)\n2. Если FTS5 пустой + semantic доступен → _semantic_candidates() (полный cosine scan)\n3. Предвычисление эмбеддинга запроса (~46 мс)\n4. Реранжирование: relevance = fts×0.3 + jaccard×0.2 + hrr×0.2 + semantic×0.3\n5. Финал: score = relevance × trust × temporal_decay\n","text",[73,1369,1366],{"__ignoreMap":71},[15,1371,1372],{},"Ключевой момент: если FTS5 возвращает 0 (запрос на русском, а факты записаны по-английски, или перефразировка), пайплайн автоматически переключается на чистый семантический поиск. Semantic — не роскошь, а load-bearing компонент для мультиязычности.",[22,1374,1376],{"id":1375},"почему-не-готовые-решения","Почему не готовые решения",[137,1378,1380],{"id":1379},"chromadb","ChromaDB",[15,1382,1383],{},"Два процесса (Chroma + Hermes), ~800 МБ до того, как агент хоть что-то запомнил. На Potato VPS — это половина RAM. Плюс Chroma тянет за собой HNSW, который строит индекс в памяти. Для фактов (сотни, может тысячи записей) — это стрельба из пушки по воробьям.",[137,1385,1387],{"id":1386},"pinecone","Pinecone",[15,1389,1390],{},"SaaS. Требует API-ключ, интернет, и доверие к третьей стороне с данными агента. Для хобби-проекта на дешёвом VPS — overkill и vendor lock-in.",[137,1392,1394],{"id":1393},"faiss","FAISS",[15,1396,1397],{},"Отличная библиотека для векторного поиска, но без полнотекстовой индексации. Придётся поверх городить FTS5 отдельно. А если уж использовать SQLite и для текста, и для векторов — зачем FAISS?",[137,1399,1401],{"id":1400},"своё-решение","Своё решение",[15,1403,1404],{},"SQLite + FTS5 + WAL — это уже есть в Python stdlib (sqlite3). Добавляем fastembed для эмбеддингов и numpy для HRR-алгебры. Один файл базы, один процесс, zero infrastructure.",[22,1406,1408],{"id":1407},"выбор-модели-mpnet-vs-minilm","Выбор модели: mpnet vs MiniLM",[15,1410,1411],{},"Я тестировал две модели multilingual fastembed:",[1265,1413,1414,1426],{},[1268,1415,1416],{},[1271,1417,1418,1420,1423],{},[1274,1419],{},[1274,1421,1422],{},"mpnet-base-v2",[1274,1424,1425],{},"MiniLM-L12-v2",[1287,1427,1428,1439,1450,1461,1472,1483,1494],{},[1271,1429,1430,1433,1436],{},[1292,1431,1432],{},"Размерность",[1292,1434,1435],{},"768",[1292,1437,1438],{},"384",[1271,1440,1441,1444,1447],{},[1292,1442,1443],{},"RSS (загружена)",[1292,1445,1446],{},"1440 МБ",[1292,1448,1449],{},"680 МБ",[1271,1451,1452,1455,1458],{},[1292,1453,1454],{},"RSS (residual после выгрузки)",[1292,1456,1457],{},"693 МБ",[1292,1459,1460],{},"481 МБ",[1271,1462,1463,1466,1469],{},[1292,1464,1465],{},"Время эмбеддинга",[1292,1467,1468],{},"65 мс",[1292,1470,1471],{},"46 мс",[1271,1473,1474,1477,1480],{},[1292,1475,1476],{},"Перезагрузка после выгрузки",[1292,1478,1479],{},"20–25 с",[1292,1481,1482],{},"1.33 с",[1271,1484,1485,1488,1491],{},[1292,1486,1487],{},"sim(\"компактный формат\" ↔ \"лаконичные сообщения\")",[1292,1489,1490],{},"0.625",[1292,1492,1493],{},"0.511",[1271,1495,1496,1499,1502],{},[1292,1497,1498],{},"sim(\"компактный формат\" ↔ \"погода для прогулки\")",[1292,1500,1501],{},"0.225",[1292,1503,1504],{},"0.008",[15,1506,1507],{},"MiniLM: 680 МБ RSS, 481 МБ residual. mpnet: 1440 МБ RSS, 693 МБ residual. На Potato VPS mpnet не влезает — после загрузки модели + Hermes + системы остаётся ~300 МБ, и OOM-killer стучится.",[15,1509,1510],{},"MiniLM даёт 0.511 для семантически схожих фраз и 0.008 для несвязанных — separation достаточная. Не идеальная (mpnet лучше на ~20%), но работоспособная.",[15,1512,1513,1516,1517,1520],{},[478,1514,1515],{},"Residual"," — это ONNX Runtime, который не освобождает пуллы памяти даже после ",[73,1518,1519],{},"del model + gc.collect()",". 481 МБ — плата за один вызов fastembed за жизнь процесса. Поэтому стратегия: lazy-load при первом запросе, держать в памяти до shutdown.",[22,1522,1524],{"id":1523},"lazy-loading-и-lifecycle-модели","Lazy-loading и lifecycle модели",[66,1526,1530],{"className":1527,"code":1528,"language":1529,"meta":71,"style":71},"language-python shiki shiki-themes github-light catppuccin-mocha","_model = None\n_available: Optional[bool] = None\n\ndef is_available() -> bool:\n    \"\"\"Проверка без загрузки модели.\"\"\"\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 при первом вызове embed_text().\"\"\"\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",[73,1531,1532,1542,1570,1574,1594,1599,1618,1626,1634,1643,1654,1666,1676,1684,1689,1700,1706,1715,1729,1744,1765,1772,1777,1809,1822,1856],{"__ignoreMap":71},[76,1533,1534,1537,1539],{"class":78,"line":79},[76,1535,1536],{"class":107},"_model ",[76,1538,112],{"class":111},[76,1540,1541],{"class":189}," None\n",[76,1543,1544,1547,1551,1555,1558,1562,1565,1568],{"class":78,"line":118},[76,1545,1546],{"class":107},"_available",[76,1548,1550],{"class":1549},"s_QEy",":",[76,1552,1554],{"class":1553},"sO2U0"," Optional",[76,1556,1557],{"class":1549},"[",[76,1559,1561],{"class":1560},"smIoM","bool",[76,1563,1564],{"class":1549},"]",[76,1566,1567],{"class":111}," =",[76,1569,1541],{"class":189},[76,1571,1572],{"class":78,"line":349},[76,1573,346],{"emptyLinePlaceholder":345},[76,1575,1576,1579,1582,1585,1588,1591],{"class":78,"line":356},[76,1577,1578],{"class":103},"def",[76,1580,1581],{"class":82}," is_available",[76,1583,1584],{"class":1549},"()",[76,1586,1587],{"class":1549}," ->",[76,1589,1590],{"class":1560}," bool",[76,1592,1593],{"class":1549},":\n",[76,1595,1596],{"class":78,"line":365},[76,1597,1598],{"class":86},"    \"\"\"Проверка без загрузки модели.\"\"\"\n",[76,1600,1601,1604,1607,1610,1613,1616],{"class":78,"line":370},[76,1602,1603],{"class":103},"    if",[76,1605,1606],{"class":107}," _available ",[76,1608,1609],{"class":103},"is",[76,1611,1612],{"class":103}," not",[76,1614,1615],{"class":189}," None",[76,1617,1593],{"class":1549},[76,1619,1620,1623],{"class":78,"line":376},[76,1621,1622],{"class":103},"        return",[76,1624,1625],{"class":107}," _available\n",[76,1627,1629,1632],{"class":78,"line":1628},8,[76,1630,1631],{"class":103},"    try",[76,1633,1593],{"class":1549},[76,1635,1637,1640],{"class":78,"line":1636},9,[76,1638,1639],{"class":103},"        import",[76,1641,1642],{"class":107}," fastembed\n",[76,1644,1646,1649,1651],{"class":78,"line":1645},10,[76,1647,1648],{"class":107},"        _available ",[76,1650,112],{"class":111},[76,1652,1653],{"class":189}," True\n",[76,1655,1657,1660,1664],{"class":78,"line":1656},11,[76,1658,1659],{"class":103},"    except",[76,1661,1663],{"class":1662},"sPY-v"," ImportError",[76,1665,1593],{"class":1549},[76,1667,1669,1671,1673],{"class":78,"line":1668},12,[76,1670,1648],{"class":107},[76,1672,112],{"class":111},[76,1674,1675],{"class":189}," False\n",[76,1677,1679,1682],{"class":78,"line":1678},13,[76,1680,1681],{"class":103},"    return",[76,1683,1625],{"class":107},[76,1685,1687],{"class":78,"line":1686},14,[76,1688,346],{"emptyLinePlaceholder":345},[76,1690,1692,1694,1697],{"class":78,"line":1691},15,[76,1693,1578],{"class":103},[76,1695,1696],{"class":82}," _get_model",[76,1698,1699],{"class":1549},"():\n",[76,1701,1703],{"class":78,"line":1702},16,[76,1704,1705],{"class":86},"    \"\"\"Lazy-load при первом вызове embed_text().\"\"\"\n",[76,1707,1709,1712],{"class":78,"line":1708},17,[76,1710,1711],{"class":103},"    global",[76,1713,1714],{"class":107}," _model\n",[76,1716,1718,1720,1723,1725,1727],{"class":78,"line":1717},18,[76,1719,1603],{"class":103},[76,1721,1722],{"class":107}," _model ",[76,1724,1609],{"class":103},[76,1726,1615],{"class":189},[76,1728,1593],{"class":1549},[76,1730,1732,1735,1738,1741],{"class":78,"line":1731},19,[76,1733,1734],{"class":103},"        from",[76,1736,1737],{"class":107}," fastembed ",[76,1739,1740],{"class":103},"import",[76,1742,1743],{"class":107}," TextEmbedding\n",[76,1745,1747,1750,1752,1756,1759,1762],{"class":78,"line":1746},20,[76,1748,1749],{"class":107},"        _model ",[76,1751,112],{"class":111},[76,1753,1755],{"class":1754},"sPNDc"," TextEmbedding",[76,1757,1758],{"class":1549},"(",[76,1760,1761],{"class":86},"\"sentence-transformers\u002Fparaphrase-multilingual-MiniLM-L12-v2\"",[76,1763,1764],{"class":1549},")\n",[76,1766,1768,1770],{"class":78,"line":1767},21,[76,1769,1681],{"class":103},[76,1771,1714],{"class":107},[76,1773,1775],{"class":78,"line":1774},22,[76,1776,346],{"emptyLinePlaceholder":345},[76,1778,1780,1782,1785,1787,1789,1791,1794,1797,1799,1802,1804,1807],{"class":78,"line":1779},23,[76,1781,1578],{"class":103},[76,1783,1784],{"class":82}," embed_text",[76,1786,1758],{"class":1549},[76,1788,1367],{"class":1553},[76,1790,1550],{"class":1549},[76,1792,1793],{"class":1560}," str",[76,1795,1796],{"class":1549},")",[76,1798,1587],{"class":1549},[76,1800,1801],{"class":107}," np",[76,1803,1354],{"class":1549},[76,1805,1806],{"class":107},"ndarray",[76,1808,1593],{"class":1549},[76,1810,1812,1815,1817,1819],{"class":78,"line":1811},24,[76,1813,1814],{"class":107},"    model ",[76,1816,112],{"class":111},[76,1818,1696],{"class":1754},[76,1820,1821],{"class":1549},"()\n",[76,1823,1825,1828,1830,1832,1834,1837,1839,1842,1845,1847,1850,1853],{"class":78,"line":1824},25,[76,1826,1827],{"class":107},"    vec ",[76,1829,112],{"class":111},[76,1831,543],{"class":1560},[76,1833,1758],{"class":1549},[76,1835,1836],{"class":107},"model",[76,1838,1354],{"class":1549},[76,1840,1841],{"class":1754},"embed",[76,1843,1844],{"class":1549},"([",[76,1846,1367],{"class":107},[76,1848,1849],{"class":1549},"]))[",[76,1851,1852],{"class":189},"0",[76,1854,1855],{"class":1549},"]\n",[76,1857,1859,1861,1864,1867,1869,1871,1874,1876,1879,1881,1884,1886],{"class":78,"line":1858},26,[76,1860,1681],{"class":103},[76,1862,1863],{"class":107}," vec ",[76,1865,1866],{"class":111},"\u002F",[76,1868,1801],{"class":107},[76,1870,1354],{"class":1549},[76,1872,1873],{"class":107},"linalg",[76,1875,1354],{"class":1549},[76,1877,1878],{"class":1754},"norm",[76,1880,1758],{"class":1549},[76,1882,1883],{"class":107},"vec",[76,1885,1796],{"class":1549},[76,1887,1888],{"class":352},"  # normalize → unit vector\n",[15,1890,1891,1892,1895],{},"Первый вызов ",[73,1893,1894],{},"embed_text()"," — ~1.3 с (загрузка ONNX-модели). Все последующие — ~46 мс. Модель живёт в памяти до shutdown.",[137,1897,1899],{"id":1898},"выгрузка-при-shutdown","Выгрузка при shutdown",[66,1901,1903],{"className":1527,"code":1902,"language":1529,"meta":71,"style":71},"# В __init__.py плагина, на shutdown hook:\nimport embedder, gc\n\ndef on_shutdown():\n    embedder._model = None\n    gc.collect()\n    # Освобождает ~200 МБ (веса модели), но ONNX residual (481 МБ) остаётся\n",[73,1904,1905,1910,1923,1927,1936,1949,1961],{"__ignoreMap":71},[76,1906,1907],{"class":78,"line":79},[76,1908,1909],{"class":352},"# В __init__.py плагина, на shutdown hook:\n",[76,1911,1912,1914,1917,1920],{"class":78,"line":118},[76,1913,1740],{"class":103},[76,1915,1916],{"class":107}," embedder",[76,1918,1919],{"class":1549},",",[76,1921,1922],{"class":107}," gc\n",[76,1924,1925],{"class":78,"line":349},[76,1926,346],{"emptyLinePlaceholder":345},[76,1928,1929,1931,1934],{"class":78,"line":356},[76,1930,1578],{"class":103},[76,1932,1933],{"class":82}," on_shutdown",[76,1935,1699],{"class":1549},[76,1937,1938,1941,1943,1945,1947],{"class":78,"line":365},[76,1939,1940],{"class":107},"    embedder",[76,1942,1354],{"class":1549},[76,1944,1536],{"class":107},[76,1946,112],{"class":111},[76,1948,1541],{"class":189},[76,1950,1951,1954,1956,1959],{"class":78,"line":370},[76,1952,1953],{"class":107},"    gc",[76,1955,1354],{"class":1549},[76,1957,1958],{"class":1754},"collect",[76,1960,1821],{"class":1549},[76,1962,1963],{"class":78,"line":376},[76,1964,1965],{"class":352},"    # Освобождает ~200 МБ (веса модели), но ONNX residual (481 МБ) остаётся\n",[15,1967,1968],{},"На практике: после shutdown RSS падает с ~1.1 ГБ до ~620 МБ. ONNX Runtime не отдаёт память — это известная особенность. 620 МБ — рабочий baseline для Potato VPS.",[22,1970,1972],{"id":1971},"хранение-два-вектора-на-факт","Хранение: два вектора на факт",[15,1974,1975],{},"Каждый факт в SQLite хранит два вектора:",[66,1977,1981],{"className":1978,"code":1979,"language":1980,"meta":71,"style":71},"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",[73,1982,1983,1997,2011,2024,2039,2053,2073,2086,2099,2107,2115,2124,2132],{"__ignoreMap":71},[76,1984,1985,1988,1991,1994],{"class":78,"line":79},[76,1986,1987],{"class":103},"CREATE",[76,1989,1990],{"class":103}," TABLE",[76,1992,1993],{"class":82}," facts",[76,1995,1996],{"class":107}," (\n",[76,1998,1999,2002,2005,2008],{"class":78,"line":118},[76,2000,2001],{"class":107},"    fact_id ",[76,2003,2004],{"class":103},"INTEGER",[76,2006,2007],{"class":103}," PRIMARY KEY",[76,2009,2010],{"class":107},",\n",[76,2012,2013,2016,2019,2022],{"class":78,"line":349},[76,2014,2015],{"class":107},"    content ",[76,2017,2018],{"class":103},"TEXT",[76,2020,2021],{"class":103}," NOT NULL",[76,2023,2010],{"class":107},[76,2025,2026,2029,2031,2034,2037],{"class":78,"line":356},[76,2027,2028],{"class":107},"    category ",[76,2030,2018],{"class":103},[76,2032,2033],{"class":103}," DEFAULT",[76,2035,2036],{"class":86}," 'general'",[76,2038,2010],{"class":107},[76,2040,2041,2044,2046,2048,2051],{"class":78,"line":365},[76,2042,2043],{"class":107},"    tags ",[76,2045,2018],{"class":103},[76,2047,2033],{"class":103},[76,2049,2050],{"class":86}," ''",[76,2052,2010],{"class":107},[76,2054,2055,2058,2061,2063,2066,2068,2071],{"class":78,"line":370},[76,2056,2057],{"class":107},"    trust_score ",[76,2059,2060],{"class":103},"REAL",[76,2062,2033],{"class":103},[76,2064,2065],{"class":189}," 0",[76,2067,1354],{"class":107},[76,2069,2070],{"class":189},"5",[76,2072,2010],{"class":107},[76,2074,2075,2078,2080,2082,2084],{"class":78,"line":376},[76,2076,2077],{"class":107},"    retrieval_count ",[76,2079,2004],{"class":103},[76,2081,2033],{"class":103},[76,2083,2065],{"class":189},[76,2085,2010],{"class":107},[76,2087,2088,2091,2093,2095,2097],{"class":78,"line":1628},[76,2089,2090],{"class":107},"    helpful_count ",[76,2092,2004],{"class":103},[76,2094,2033],{"class":103},[76,2096,2065],{"class":189},[76,2098,2010],{"class":107},[76,2100,2101,2104],{"class":78,"line":1636},[76,2102,2103],{"class":107},"    hrr_vector BLOB,        ",[76,2105,2106],{"class":352},"-- 1024 × float64 = 8192 bytes\n",[76,2108,2109,2112],{"class":78,"line":1645},[76,2110,2111],{"class":107},"    semantic_vector BLOB,   ",[76,2113,2114],{"class":352},"-- 384 × float32 = 1536 bytes\n",[76,2116,2117,2120,2122],{"class":78,"line":1656},[76,2118,2119],{"class":107},"    created_at ",[76,2121,2018],{"class":103},[76,2123,2010],{"class":107},[76,2125,2126,2129],{"class":78,"line":1668},[76,2127,2128],{"class":107},"    updated_at ",[76,2130,2131],{"class":103},"TEXT\n",[76,2133,2134],{"class":78,"line":1678},[76,2135,2136],{"class":107},");\n",[15,2138,2139,2142],{},[478,2140,2141],{},"HRR-вектор"," (8 КБ на факт) — SHA-256 phase encoding. Токены контента → бандл атомов → bind с ROLE_CONTENT и ROLE_ENTITY. Используется для алгебраических операций: probe («все факты про X»), related («что связано с X»), reason («что общего у X, Y, Z»). Работает на numpy, без модели.",[15,2144,2145,2148],{},[478,2146,2147],{},"Semantic-вектор"," (1.5 КБ на факт) — выход fastembed. Нормализованный unit vector для cosine similarity. Используется в гибридном скоринге.",[15,2150,2151],{},"Итого: ~9.7 КБ на факт. Для 1000 фактов — ~10 МБ. Пустяк.",[22,2153,2155],{"id":2154},"hrr-композиционная-алгебра-без-нейросетей","HRR: композиционная алгебра без нейросетей",[15,2157,2158],{},"HRR (Holographic Reduced Representations) — это способ кодировать структуру в фиксированный вектор. Вместо обучения — SHA-256 хеши от токенов, преобразованные в phase vectors.",[66,2160,2162],{"className":1527,"code":2161,"language":1529,"meta":71,"style":71},"def encode_atom(token: str, dim: int = 1024) -> np.ndarray:\n    \"\"\"Токен → единичный вектор в phase space.\"\"\"\n    h = hashlib.sha256(token.encode()).digest()\n    rng = np.frombuffer(h, dtype=np.uint8).astype(np.float64)\n    # Фазовый код: cos + j*sin → unit vector в комплексном пространстве\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    \"\"\"Связывание: circular convolution в frequency domain.\"\"\"\n    return np.fft.ifft(np.fft.fft(a) * np.fft.fft(b))\n\ndef bundle(vectors):\n    \"\"\"Бандлинг: поэлементная сумма + нормализация.\"\"\"\n    result = np.sum(vectors, axis=0)\n    return result \u002F np.linalg.norm(result)\n",[73,2163,2164,2207,2212,2244,2296,2301,2340,2382,2386,2405,2410,2464,2468,2482,2487,2516],{"__ignoreMap":71},[76,2165,2166,2168,2171,2173,2176,2178,2180,2182,2185,2187,2190,2192,2195,2197,2199,2201,2203,2205],{"class":78,"line":79},[76,2167,1578],{"class":103},[76,2169,2170],{"class":82}," encode_atom",[76,2172,1758],{"class":1549},[76,2174,2175],{"class":1553},"token",[76,2177,1550],{"class":1549},[76,2179,1793],{"class":1560},[76,2181,1919],{"class":1549},[76,2183,2184],{"class":1553}," dim",[76,2186,1550],{"class":1549},[76,2188,2189],{"class":1560}," int",[76,2191,1567],{"class":111},[76,2193,2194],{"class":189}," 1024",[76,2196,1796],{"class":1549},[76,2198,1587],{"class":1549},[76,2200,1801],{"class":107},[76,2202,1354],{"class":1549},[76,2204,1806],{"class":107},[76,2206,1593],{"class":1549},[76,2208,2209],{"class":78,"line":118},[76,2210,2211],{"class":86},"    \"\"\"Токен → единичный вектор в phase space.\"\"\"\n",[76,2213,2214,2217,2219,2222,2224,2227,2229,2231,2233,2236,2239,2242],{"class":78,"line":349},[76,2215,2216],{"class":107},"    h ",[76,2218,112],{"class":111},[76,2220,2221],{"class":107}," hashlib",[76,2223,1354],{"class":1549},[76,2225,2226],{"class":1754},"sha256",[76,2228,1758],{"class":1549},[76,2230,2175],{"class":107},[76,2232,1354],{"class":1549},[76,2234,2235],{"class":1754},"encode",[76,2237,2238],{"class":1549},"()).",[76,2240,2241],{"class":1754},"digest",[76,2243,1821],{"class":1549},[76,2245,2246,2249,2251,2253,2255,2258,2260,2263,2265,2269,2271,2274,2276,2279,2282,2285,2287,2289,2291,2294],{"class":78,"line":356},[76,2247,2248],{"class":107},"    rng ",[76,2250,112],{"class":111},[76,2252,1801],{"class":107},[76,2254,1354],{"class":1549},[76,2256,2257],{"class":1754},"frombuffer",[76,2259,1758],{"class":1549},[76,2261,2262],{"class":107},"h",[76,2264,1919],{"class":1549},[76,2266,2268],{"class":2267},"s-dMd"," dtype",[76,2270,112],{"class":111},[76,2272,2273],{"class":107},"np",[76,2275,1354],{"class":1549},[76,2277,2278],{"class":107},"uint8",[76,2280,2281],{"class":1549},").",[76,2283,2284],{"class":1754},"astype",[76,2286,1758],{"class":1549},[76,2288,2273],{"class":107},[76,2290,1354],{"class":1549},[76,2292,2293],{"class":107},"float64",[76,2295,1764],{"class":1549},[76,2297,2298],{"class":78,"line":365},[76,2299,2300],{"class":352},"    # Фазовый код: cos + j*sin → unit vector в комплексном пространстве\n",[76,2302,2303,2306,2308,2311,2314,2317,2319,2322,2325,2328,2331,2333,2335,2337],{"class":78,"line":370},[76,2304,2305],{"class":107},"    phases ",[76,2307,112],{"class":111},[76,2309,2310],{"class":1553}," rng",[76,2312,2313],{"class":1549},"[:",[76,2315,2316],{"class":1553},"dim",[76,2318,1564],{"class":1549},[76,2320,2321],{"class":111}," \u002F",[76,2323,2324],{"class":189}," 255.0",[76,2326,2327],{"class":111}," *",[76,2329,2330],{"class":189}," 2",[76,2332,2327],{"class":111},[76,2334,1801],{"class":107},[76,2336,1354],{"class":1549},[76,2338,2339],{"class":107},"pi\n",[76,2341,2342,2344,2346,2348,2351,2353,2356,2358,2361,2364,2367,2369,2371,2373,2376,2378,2380],{"class":78,"line":376},[76,2343,1681],{"class":103},[76,2345,1801],{"class":107},[76,2347,1354],{"class":1549},[76,2349,2350],{"class":1754},"cos",[76,2352,1758],{"class":1549},[76,2354,2355],{"class":107},"phases",[76,2357,1796],{"class":1549},[76,2359,2360],{"class":111}," +",[76,2362,2363],{"class":189}," 1",[76,2365,2366],{"class":103},"j",[76,2368,2327],{"class":111},[76,2370,1801],{"class":107},[76,2372,1354],{"class":1549},[76,2374,2375],{"class":1754},"sin",[76,2377,1758],{"class":1549},[76,2379,2355],{"class":107},[76,2381,1764],{"class":1549},[76,2383,2384],{"class":78,"line":1628},[76,2385,346],{"emptyLinePlaceholder":345},[76,2387,2388,2390,2393,2395,2397,2399,2402],{"class":78,"line":1636},[76,2389,1578],{"class":103},[76,2391,2392],{"class":82}," bind",[76,2394,1758],{"class":1549},[76,2396,58],{"class":1553},[76,2398,1919],{"class":1549},[76,2400,2401],{"class":1553}," b",[76,2403,2404],{"class":1549},"):\n",[76,2406,2407],{"class":78,"line":1645},[76,2408,2409],{"class":86},"    \"\"\"Связывание: circular convolution в frequency domain.\"\"\"\n",[76,2411,2412,2414,2416,2418,2421,2423,2426,2428,2430,2432,2434,2436,2438,2440,2442,2444,2446,2448,2450,2452,2454,2456,2458,2461],{"class":78,"line":1656},[76,2413,1681],{"class":103},[76,2415,1801],{"class":107},[76,2417,1354],{"class":1549},[76,2419,2420],{"class":107},"fft",[76,2422,1354],{"class":1549},[76,2424,2425],{"class":1754},"ifft",[76,2427,1758],{"class":1549},[76,2429,2273],{"class":107},[76,2431,1354],{"class":1549},[76,2433,2420],{"class":107},[76,2435,1354],{"class":1549},[76,2437,2420],{"class":1754},[76,2439,1758],{"class":1549},[76,2441,58],{"class":107},[76,2443,1796],{"class":1549},[76,2445,2327],{"class":111},[76,2447,1801],{"class":107},[76,2449,1354],{"class":1549},[76,2451,2420],{"class":107},[76,2453,1354],{"class":1549},[76,2455,2420],{"class":1754},[76,2457,1758],{"class":1549},[76,2459,2460],{"class":107},"b",[76,2462,2463],{"class":1549},"))\n",[76,2465,2466],{"class":78,"line":1668},[76,2467,346],{"emptyLinePlaceholder":345},[76,2469,2470,2472,2475,2477,2480],{"class":78,"line":1678},[76,2471,1578],{"class":103},[76,2473,2474],{"class":82}," bundle",[76,2476,1758],{"class":1549},[76,2478,2479],{"class":1553},"vectors",[76,2481,2404],{"class":1549},[76,2483,2484],{"class":78,"line":1686},[76,2485,2486],{"class":86},"    \"\"\"Бандлинг: поэлементная сумма + нормализация.\"\"\"\n",[76,2488,2489,2492,2494,2496,2498,2501,2503,2505,2507,2510,2512,2514],{"class":78,"line":1691},[76,2490,2491],{"class":107},"    result ",[76,2493,112],{"class":111},[76,2495,1801],{"class":107},[76,2497,1354],{"class":1549},[76,2499,2500],{"class":1754},"sum",[76,2502,1758],{"class":1549},[76,2504,2479],{"class":107},[76,2506,1919],{"class":1549},[76,2508,2509],{"class":2267}," axis",[76,2511,112],{"class":111},[76,2513,1852],{"class":189},[76,2515,1764],{"class":1549},[76,2517,2518,2520,2523,2525,2527,2529,2531,2533,2535,2537,2540],{"class":78,"line":1702},[76,2519,1681],{"class":103},[76,2521,2522],{"class":107}," result ",[76,2524,1866],{"class":111},[76,2526,1801],{"class":107},[76,2528,1354],{"class":1549},[76,2530,1873],{"class":107},[76,2532,1354],{"class":1549},[76,2534,1878],{"class":1754},[76,2536,1758],{"class":1549},[76,2538,2539],{"class":107},"result",[76,2541,1764],{"class":1549},[15,2543,2544,2545,2548],{},"Зачем это нужно, если есть fastembed? Потому что HRR поддерживает ",[478,2546,2547],{},"алгебраические операции",", которые векторные модели не умеют:",[2550,2551,2552,2558,2564],"ul",{},[33,2553,2554,2557],{},[478,2555,2556],{},"probe(entity)"," — «дай все факты, привязанные к сущности X». Bind\u002Funbind с entity-атомом.",[33,2559,2560,2563],{},[478,2561,2562],{},"related(entity)"," — «что структурно соседствует с X». Через HRR similarity.",[33,2565,2566,2572,2573,1354],{},[478,2567,2568,2569,1796],{},"reason(",[76,2570,2571],{},"e1, e2, e3"," — «что общего у нескольких сущностей». Multi-entity JOIN, ",[73,2574,2575],{},"min(scores)",[15,2577,2578],{},"Semantic-модель даёт «похожесть по смыслу», HRR — «связанность по структуре». Это разные оси.",[22,2580,2582],{"id":2581},"jaccard-дешёвый-фильтр","Jaccard: дешёвый фильтр",[15,2584,2585],{},"Jaccard — пересечение токенов запроса и факта, поделённое на объединение. Стоимость: O(n) по токенам, ноль выделений памяти. Работает как грубый фильтр: если запрос и факт не пересекаются ни одним токеном — штраф.",[66,2587,2589],{"className":1527,"code":2588,"language":1529,"meta":71,"style":71},"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",[73,2590,2591,2626,2644,2651],{"__ignoreMap":71},[76,2592,2593,2595,2598,2600,2603,2605,2608,2610,2613,2615,2617,2619,2621,2624],{"class":78,"line":79},[76,2594,1578],{"class":103},[76,2596,2597],{"class":82}," jaccard",[76,2599,1758],{"class":1549},[76,2601,2602],{"class":1553},"query_tokens",[76,2604,1550],{"class":1549},[76,2606,2607],{"class":1560}," set",[76,2609,1919],{"class":1549},[76,2611,2612],{"class":1553}," fact_tokens",[76,2614,1550],{"class":1549},[76,2616,2607],{"class":1560},[76,2618,1796],{"class":1549},[76,2620,1587],{"class":1549},[76,2622,2623],{"class":1560}," float",[76,2625,1593],{"class":1549},[76,2627,2628,2630,2632,2635,2638,2640,2642],{"class":78,"line":118},[76,2629,1603],{"class":103},[76,2631,1612],{"class":103},[76,2633,2634],{"class":107}," query_tokens ",[76,2636,2637],{"class":103},"or",[76,2639,1612],{"class":103},[76,2641,2612],{"class":107},[76,2643,1593],{"class":1549},[76,2645,2646,2648],{"class":78,"line":349},[76,2647,1622],{"class":103},[76,2649,2650],{"class":189}," 0.0\n",[76,2652,2653,2655,2658,2660,2663,2666,2668,2670,2672,2674,2676,2678,2681,2683],{"class":78,"line":356},[76,2654,1681],{"class":103},[76,2656,2657],{"class":1662}," len",[76,2659,1758],{"class":1549},[76,2661,2662],{"class":107},"query_tokens ",[76,2664,2665],{"class":111},"&",[76,2667,2612],{"class":107},[76,2669,1796],{"class":1549},[76,2671,2321],{"class":111},[76,2673,2657],{"class":1662},[76,2675,1758],{"class":1549},[76,2677,2662],{"class":107},[76,2679,2680],{"class":111},"|",[76,2682,2612],{"class":107},[76,2684,1764],{"class":1549},[15,2686,2687],{},"Вес 0.2 — не главный канал, но отсекает шум. На практике: запрос «деплой nginx» и факт «настройка DNS» получают jaccard=0, и это правильно.",[22,2689,2691],{"id":2690},"fts5-полнотекстовая-индексация","FTS5: полнотекстовая индексация",[15,2693,2694],{},"SQLite FTS5 — встроенная полнотекстовая индексация. AND-семантика: все слова запроса должны присутствовать в факте. Это жёстко, но предсказуемо.",[66,2696,2698],{"className":1978,"code":2697,"language":1980,"meta":71,"style":71},"CREATE VIRTUAL TABLE facts_fts USING fts5(content, content=facts, content_rowid=fact_id);\n\n-- Поиск:\nSELECT fact_id, rank FROM facts_fts WHERE facts_fts MATCH 'деплой nginx'\nORDER BY rank LIMIT 30;\n",[73,2699,2700,2729,2733,2738,2762],{"__ignoreMap":71},[76,2701,2702,2704,2707,2710,2713,2716,2719,2721,2724,2726],{"class":78,"line":79},[76,2703,1987],{"class":103},[76,2705,2706],{"class":107}," VIRTUAL ",[76,2708,2709],{"class":103},"TABLE",[76,2711,2712],{"class":107}," facts_fts ",[76,2714,2715],{"class":103},"USING",[76,2717,2718],{"class":107}," fts5(content, content",[76,2720,112],{"class":111},[76,2722,2723],{"class":107},"facts, content_rowid",[76,2725,112],{"class":111},[76,2727,2728],{"class":107},"fact_id);\n",[76,2730,2731],{"class":78,"line":118},[76,2732,346],{"emptyLinePlaceholder":345},[76,2734,2735],{"class":78,"line":349},[76,2736,2737],{"class":352},"-- Поиск:\n",[76,2739,2740,2743,2746,2749,2751,2754,2756,2759],{"class":78,"line":356},[76,2741,2742],{"class":103},"SELECT",[76,2744,2745],{"class":107}," fact_id, rank ",[76,2747,2748],{"class":103},"FROM",[76,2750,2712],{"class":107},[76,2752,2753],{"class":103},"WHERE",[76,2755,2712],{"class":107},[76,2757,2758],{"class":103},"MATCH",[76,2760,2761],{"class":86}," 'деплой nginx'\n",[76,2763,2764,2767,2770,2773,2776],{"class":78,"line":365},[76,2765,2766],{"class":103},"ORDER BY",[76,2768,2769],{"class":107}," rank ",[76,2771,2772],{"class":103},"LIMIT",[76,2774,2775],{"class":189}," 30",[76,2777,2778],{"class":107},";\n",[15,2780,2781],{},"Проблема FTS5: не понимает перефразировок. «Как выкатить nginx на прод» не матчит «деплой nginx через CI\u002FCD». Поэтому fallback на semantic search при пустом FTS5 — критичен.",[22,2783,2785],{"id":2784},"стоимость-на-potato-vps","Стоимость на Potato VPS",[15,2787,2788],{},"Типичный memory footprint при работающем агенте:",[1265,2790,2791,2801],{},[1268,2792,2793],{},[1271,2794,2795,2798],{},[1274,2796,2797],{},"Компонент",[1274,2799,2800],{},"RSS",[1287,2802,2803,2811,2819,2827,2835],{},[1271,2804,2805,2808],{},[1292,2806,2807],{},"Hermes core (Python)",[1292,2809,2810],{},"~380 МБ",[1271,2812,2813,2816],{},[1292,2814,2815],{},"fastembed (загружена)",[1292,2817,2818],{},"+300 МБ",[1271,2820,2821,2824],{},[1292,2822,2823],{},"ONNX Runtime residual",[1292,2825,2826],{},"(включено выше)",[1271,2828,2829,2832],{},[1292,2830,2831],{},"SQLite + FTS5 index",[1292,2833,2834],{},"~5 МБ",[1271,2836,2837,2840],{},[1292,2838,2839],{},"Итого",[1292,2841,2842],{},"~680 МБ",[15,2844,2845],{},"Остаётся достаточно для системы, swap, и других процессов. Комфортно.",[15,2847,2848,2849,2852],{},"При shutdown модель выгружается, RSS падает до ~620 МБ. ONNX residual не освобождается — это цена одного ",[73,2850,2851],{},"import fastembed"," за жизнь процесса.",[22,2854,2856],{"id":2855},"weight-auto-redistribution","Weight auto-redistribution",[15,2858,2859],{},"Если fastembed недоступна (не установлена, или numpy отсутствует), веса перераспределяются автоматически:",[66,2861,2863],{"className":1527,"code":2862,"language":1529,"meta":71,"style":71},"def _redistribute_weights(self):\n    if not embedder.is_available():\n        # Semantic недоступен: 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 недоступен: 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",[73,2864,2865,2880,2895,2900,2916,2930,2944,2957,2970,2975,2988,3001,3013],{"__ignoreMap":71},[76,2866,2867,2869,2872,2874,2878],{"class":78,"line":79},[76,2868,1578],{"class":103},[76,2870,2871],{"class":82}," _redistribute_weights",[76,2873,1758],{"class":1549},[76,2875,2877],{"class":2876},"s7pD5","self",[76,2879,2404],{"class":1549},[76,2881,2882,2884,2886,2888,2890,2893],{"class":78,"line":118},[76,2883,1603],{"class":103},[76,2885,1612],{"class":103},[76,2887,1916],{"class":107},[76,2889,1354],{"class":1549},[76,2891,2892],{"class":1754},"is_available",[76,2894,1699],{"class":1549},[76,2896,2897],{"class":78,"line":349},[76,2898,2899],{"class":352},"        # Semantic недоступен: 0.3 → FTS +0.15, Jaccard +0.1, HRR +0.05\n",[76,2901,2902,2906,2908,2911,2913],{"class":78,"line":356},[76,2903,2905],{"class":2904},"sG_o1","        self",[76,2907,1354],{"class":1549},[76,2909,2910],{"class":107},"fts_weight ",[76,2912,112],{"class":111},[76,2914,2915],{"class":189}," 0.45\n",[76,2917,2918,2920,2922,2925,2927],{"class":78,"line":365},[76,2919,2905],{"class":2904},[76,2921,1354],{"class":1549},[76,2923,2924],{"class":107},"jaccard_weight ",[76,2926,112],{"class":111},[76,2928,2929],{"class":189}," 0.30\n",[76,2931,2932,2934,2936,2939,2941],{"class":78,"line":370},[76,2933,2905],{"class":2904},[76,2935,1354],{"class":1549},[76,2937,2938],{"class":107},"hrr_weight ",[76,2940,112],{"class":111},[76,2942,2943],{"class":189}," 0.25\n",[76,2945,2946,2948,2950,2953,2955],{"class":78,"line":376},[76,2947,2905],{"class":2904},[76,2949,1354],{"class":1549},[76,2951,2952],{"class":107},"semantic_weight ",[76,2954,112],{"class":111},[76,2956,2650],{"class":189},[76,2958,2959,2962,2964,2968],{"class":78,"line":1628},[76,2960,2961],{"class":103},"    elif",[76,2963,1612],{"class":103},[76,2965,2967],{"class":2966},"sbIxs"," _HAS_NUMPY",[76,2969,1593],{"class":1549},[76,2971,2972],{"class":78,"line":1636},[76,2973,2974],{"class":352},"        # HRR недоступен: 0.2 → FTS +0.1, Semantic +0.1\n",[76,2976,2977,2979,2981,2983,2985],{"class":78,"line":1645},[76,2978,2905],{"class":2904},[76,2980,1354],{"class":1549},[76,2982,2910],{"class":107},[76,2984,112],{"class":111},[76,2986,2987],{"class":189}," 0.40\n",[76,2989,2990,2992,2994,2996,2998],{"class":78,"line":1656},[76,2991,2905],{"class":2904},[76,2993,1354],{"class":1549},[76,2995,2924],{"class":107},[76,2997,112],{"class":111},[76,2999,3000],{"class":189}," 0.20\n",[76,3002,3003,3005,3007,3009,3011],{"class":78,"line":1668},[76,3004,2905],{"class":2904},[76,3006,1354],{"class":1549},[76,3008,2938],{"class":107},[76,3010,112],{"class":111},[76,3012,2650],{"class":189},[76,3014,3015,3017,3019,3021,3023],{"class":78,"line":1678},[76,3016,2905],{"class":2904},[76,3018,1354],{"class":1549},[76,3020,2952],{"class":107},[76,3022,112],{"class":111},[76,3024,2987],{"class":189},[15,3026,3027],{},"Graceful degradation: система работает и без эмбеддингов (FTS + Jaccard), и без HRR (FTS + Jaccard + Semantic). Но полная четвёрка — оптимальный режим.",[22,3029,3031],{"id":3030},"практические-грабли","Практические грабли",[137,3033,3035],{"id":3034},"_1-fts5-and-слишком-строго","1. FTS5 AND — слишком строго",[15,3037,3038],{},"Запрос «компактный формат сообщений» требует наличия всех трёх слов. Если факт записан как «лаконичные ответы» — FTS5 молчит. Semantic спасает, но только если модель загружена.",[15,3040,3041,3044],{},[478,3042,3043],{},"Решение:"," не полагаться на FTS5 как единственный канал. Всегда держать semantic включённым.",[137,3046,3048],{"id":3047},"_2-missing-semantic-vectors-после-обновления","2. Missing semantic vectors после обновления",[15,3050,3051,3052,3055],{},"Агент обновился, но работает старый процесс. Новые факты записываются без ",[73,3053,3054],{},"semantic_vector",". Симптом: русские запросы возвращают пустой результат.",[66,3057,3059],{"className":1978,"code":3058,"language":1980,"meta":71,"style":71},"SELECT COUNT(*) FROM facts WHERE semantic_vector IS NULL;\n",[73,3060,3061],{"__ignoreMap":71},[76,3062,3063,3065,3069,3071,3074,3077,3079,3082,3084,3087,3090,3093],{"class":78,"line":79},[76,3064,2742],{"class":103},[76,3066,3068],{"class":3067},"sRaXx"," COUNT",[76,3070,1758],{"class":107},[76,3072,3073],{"class":111},"*",[76,3075,3076],{"class":107},") ",[76,3078,2748],{"class":103},[76,3080,3081],{"class":107}," facts ",[76,3083,2753],{"class":103},[76,3085,3086],{"class":107}," semantic_vector ",[76,3088,3089],{"class":103},"IS",[76,3091,3092],{"class":103}," NULL",[76,3094,2778],{"class":107},[15,3096,3097],{},"Если > 0 — запустить backfill:",[66,3099,3101],{"className":1527,"code":3100,"language":1529,"meta":71,"style":71},"import embedder, sqlite3\n\nconn = sqlite3.connect(db_path)  # путь к вашей базе\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",[73,3102,3103,3114,3118,3143,3170,3174,3195,3215,3231,3255,3259],{"__ignoreMap":71},[76,3104,3105,3107,3109,3111],{"class":78,"line":79},[76,3106,1740],{"class":103},[76,3108,1916],{"class":107},[76,3110,1919],{"class":1549},[76,3112,3113],{"class":107}," sqlite3\n",[76,3115,3116],{"class":78,"line":118},[76,3117,346],{"emptyLinePlaceholder":345},[76,3119,3120,3123,3125,3128,3130,3133,3135,3138,3140],{"class":78,"line":349},[76,3121,3122],{"class":107},"conn ",[76,3124,112],{"class":111},[76,3126,3127],{"class":107}," sqlite3",[76,3129,1354],{"class":1549},[76,3131,3132],{"class":1754},"connect",[76,3134,1758],{"class":1549},[76,3136,3137],{"class":107},"db_path",[76,3139,1796],{"class":1549},[76,3141,3142],{"class":352},"  # путь к вашей базе\n",[76,3144,3145,3148,3150,3153,3155,3158,3160,3163,3165,3168],{"class":78,"line":356},[76,3146,3147],{"class":107},"rows ",[76,3149,112],{"class":111},[76,3151,3152],{"class":107}," conn",[76,3154,1354],{"class":1549},[76,3156,3157],{"class":1754},"execute",[76,3159,1758],{"class":1549},[76,3161,3162],{"class":86},"'SELECT fact_id, content FROM facts WHERE semantic_vector IS NULL'",[76,3164,2281],{"class":1549},[76,3166,3167],{"class":1754},"fetchall",[76,3169,1821],{"class":1549},[76,3171,3172],{"class":78,"line":365},[76,3173,346],{"emptyLinePlaceholder":345},[76,3175,3176,3179,3182,3184,3187,3190,3193],{"class":78,"line":370},[76,3177,3178],{"class":103},"for",[76,3180,3181],{"class":107}," fid",[76,3183,1919],{"class":1549},[76,3185,3186],{"class":107}," content ",[76,3188,3189],{"class":103},"in",[76,3191,3192],{"class":107}," rows",[76,3194,1593],{"class":1549},[76,3196,3197,3199,3201,3203,3205,3208,3210,3213],{"class":78,"line":376},[76,3198,1827],{"class":107},[76,3200,112],{"class":111},[76,3202,1916],{"class":107},[76,3204,1354],{"class":1549},[76,3206,3207],{"class":1754},"embed_text",[76,3209,1758],{"class":1549},[76,3211,3212],{"class":107},"content",[76,3214,1764],{"class":1549},[76,3216,3217,3220,3222,3224,3226,3229],{"class":78,"line":1628},[76,3218,3219],{"class":107},"    conn",[76,3221,1354],{"class":1549},[76,3223,3157],{"class":1754},[76,3225,1758],{"class":1549},[76,3227,3228],{"class":86},"'UPDATE facts SET semantic_vector = ? WHERE fact_id = ?'",[76,3230,2010],{"class":1549},[76,3232,3233,3236,3239,3241,3244,3246,3248,3251,3253],{"class":78,"line":1636},[76,3234,3235],{"class":1549},"                 (",[76,3237,3238],{"class":107},"embedder",[76,3240,1354],{"class":1549},[76,3242,3243],{"class":1754},"vector_to_bytes",[76,3245,1758],{"class":1549},[76,3247,1883],{"class":107},[76,3249,3250],{"class":1549},"),",[76,3252,3181],{"class":107},[76,3254,2463],{"class":1549},[76,3256,3257],{"class":78,"line":1645},[76,3258,346],{"emptyLinePlaceholder":345},[76,3260,3261,3264,3266,3269],{"class":78,"line":1656},[76,3262,3263],{"class":107},"conn",[76,3265,1354],{"class":1549},[76,3267,3268],{"class":1754},"commit",[76,3270,1821],{"class":1549},[137,3272,3274],{"id":3273},"_3-fastembed-pooling-change","3. fastembed pooling change",[15,3276,3277],{},"Версия 0.8.0+ меняет pooling с CLS на mean. Старые векторы несовместимы с новыми. Решение: полный re-embed всех фактов после обновления fastembed.",[137,3279,3281],{"id":3280},"_4-onnx-memory-leak-это-не-leak","4. ONNX memory leak (это не leak)",[15,3283,3284,3286],{},[73,3285,1519],{}," освобождает веса, но не ONNX Runtime pools. Это не утечка — так устроен ONNX. 481 МБ residual — норма. Не пытайтесь «починить».",[137,3288,3290],{"id":3289},"_5-rss-used-memory","5. RSS ≠ used memory",[15,3292,3293,3296],{},[73,3294,3295],{},"ru_maxrss"," показывает пик, а не текущее потребление. Для точного измерения:",[66,3298,3300],{"className":1527,"code":3299,"language":1529,"meta":71,"style":71},"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",[73,3301,3302,3311,3334,3368],{"__ignoreMap":71},[76,3303,3304,3306,3309],{"class":78,"line":79},[76,3305,1578],{"class":103},[76,3307,3308],{"class":82}," current_rss_mb",[76,3310,1699],{"class":1549},[76,3312,3313,3316,3319,3321,3324,3326,3329,3332],{"class":78,"line":118},[76,3314,3315],{"class":103},"    with",[76,3317,3318],{"class":1662}," open",[76,3320,1758],{"class":1549},[76,3322,3323],{"class":86},"'\u002Fproc\u002Fself\u002Fstatm'",[76,3325,1796],{"class":1549},[76,3327,3328],{"class":103}," as",[76,3330,3331],{"class":107}," f",[76,3333,1593],{"class":1549},[76,3335,3336,3339,3341,3343,3345,3348,3350,3353,3356,3359,3362,3365],{"class":78,"line":349},[76,3337,3338],{"class":107},"        pages ",[76,3340,112],{"class":111},[76,3342,2189],{"class":1560},[76,3344,1758],{"class":1549},[76,3346,3347],{"class":107},"f",[76,3349,1354],{"class":1549},[76,3351,3352],{"class":1754},"read",[76,3354,3355],{"class":1549},"().",[76,3357,3358],{"class":1754},"split",[76,3360,3361],{"class":1549},"()[",[76,3363,3364],{"class":189},"1",[76,3366,3367],{"class":1549},"])\n",[76,3369,3370,3372,3375,3377,3380,3382,3384,3386],{"class":78,"line":356},[76,3371,1681],{"class":103},[76,3373,3374],{"class":107}," pages ",[76,3376,3073],{"class":111},[76,3378,3379],{"class":189}," 4096",[76,3381,2321],{"class":111},[76,3383,2194],{"class":189},[76,3385,2321],{"class":111},[76,3387,3388],{"class":189}," 1024\n",[22,3390,3392],{"id":3391},"web-интерфейс-для-просмотра-фактов","Web-интерфейс для просмотра фактов",[15,3394,3395],{},"Для отладки я сделал standalone htmx-приложение на stdlib http.server. Тёмная тема, моноширинный шрифт, FTS5-поиск, inline-редактирование, кнопки feedback, цветовые бейджи по категориям. Zero dependencies — только Python stdlib. Запускается одной командой, слушает на локальном порту.",[22,3397,2839],{"id":3398},"итого",[15,3400,3401],{},"Четыре стратегии поиска в одном SQLite-файле. 680 МБ RSS при работающей модели. Lazy-load, graceful degradation, выгрузка при shutdown. Никаких внешних сервисов, никаких docker-compose, никаких Pinecone API-ключей.",[15,3403,3404],{},"Для хобби-проекта на Potato VPS — это единственный адекватный вариант. Не потому что «лучше ChromaDB», а потому что ChromaDB не влезает в скромный объём RAM, а Pinecone — это чужой компьютер.",[15,3406,3407],{},"Свой плагин на SQLite — это контроль. Контроль над памятью, над индексацией, над lifecycle модели. И когда в 3 часа ночи OOM-killer стучится — ты точно знаешь, кто виноват и что делать.",[3409,3410],"hr",{},[15,3412,3413],{},[3414,3415,3416,3417,3419],"em",{},"Плагин ",[73,3418,1255],{}," — часть проекта Hermes Agent.",[652,3421,3422],{},"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":71,"searchDepth":118,"depth":118,"links":3424},[3425,3426,3427,3433,3434,3437,3438,3439,3440,3441,3442,3443,3450,3451],{"id":1259,"depth":118,"text":1260},{"id":1360,"depth":118,"text":1361},{"id":1375,"depth":118,"text":1376,"children":3428},[3429,3430,3431,3432],{"id":1379,"depth":349,"text":1380},{"id":1386,"depth":349,"text":1387},{"id":1393,"depth":349,"text":1394},{"id":1400,"depth":349,"text":1401},{"id":1407,"depth":118,"text":1408},{"id":1523,"depth":118,"text":1524,"children":3435},[3436],{"id":1898,"depth":349,"text":1899},{"id":1971,"depth":118,"text":1972},{"id":2154,"depth":118,"text":2155},{"id":2581,"depth":118,"text":2582},{"id":2690,"depth":118,"text":2691},{"id":2784,"depth":118,"text":2785},{"id":2855,"depth":118,"text":2856},{"id":3030,"depth":118,"text":3031,"children":3444},[3445,3446,3447,3448,3449],{"id":3034,"depth":349,"text":3035},{"id":3047,"depth":349,"text":3048},{"id":3273,"depth":349,"text":3274},{"id":3280,"depth":349,"text":3281},{"id":3289,"depth":349,"text":3290},{"id":3391,"depth":118,"text":3392},{"id":3398,"depth":118,"text":2839},"Как запустить гибридный поиск (FTS5 + Jaccard + HRR + fastembed MiniLM-L12-v2) на дешёвом VPS. Lazy-loading модели, ~680 MB RSS, выгрузка при shutdown. Почему не ChromaDB и не Pinecone — а свой плагин на SQLite.",{},"\u002Fblog\u002Fholographic-memory-potato-vps",{"title":1238,"description":3452},"blog\u002Fholographic-memory-potato-vps",[3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473],"ai-memory","vector-search","embeddings","sqlite","fts5","fastembed","semantic-search","self-hosted","potato-vps","rag","hybrid-search","nlp","mini-lm","hrr","jaccard","performance-optimization","7QeOSdwwmzCmc7K1aKbRemQMoO0eS3ZCz91ujdION3M",{"id":3476,"title":3477,"body":3478,"date":671,"description":5352,"extension":673,"meta":5353,"navigation":345,"path":5354,"readingTime":676,"seo":5355,"stem":5356,"tags":5357,"__hash__":5358},"articles\u002Fblog\u002Fholographic-memory-potato-vps.en.md","Holographic Memory for an AI Agent on a Potato VPS",{"type":8,"value":3479,"toc":5323},[3480,3483,3486,3489,3495,3499,3502,3566,3574,3577,3581,3587,3590,3594,3596,3599,3601,3604,3606,3609,3613,3616,3620,3623,3708,3711,3714,3722,3726,3999,4005,4009,4068,4071,4075,4078,4206,4212,4218,4221,4225,4228,4561,4568,4589,4592,4596,4599,4685,4688,4692,4695,4762,4765,4769,4772,4822,4825,4831,4834,4837,4984,4987,4991,4995,4998,5004,5008,5014,5044,5047,5195,5198,5201,5205,5210,5213,5218,5292,5296,5299,5303,5306,5309,5312,5314,5321],[11,3481,3477],{"id":3482},"holographic-memory-for-an-ai-agent-on-a-potato-vps",[15,3484,3485],{},"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.",[15,3487,3488],{},"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.",[15,3490,3491,3492,3494],{},"The solution is the ",[73,3493,1255],{}," plugin — four search strategies in a single SQLite file. Here's how it works and why.",[22,3496,3498],{"id":3497},"architecture-four-strategies-one-query","Architecture: Four Strategies, One Query",[15,3500,3501],{},"Hybrid scoring isn't \"vector search with FTS fallback\" — it's four independent channels whose results are combined with weights:",[1265,3503,3504,3520],{},[1268,3505,3506],{},[1271,3507,3508,3511,3514,3517],{},[1274,3509,3510],{},"Strategy",[1274,3512,3513],{},"Weight",[1274,3515,3516],{},"What it does",[1274,3518,3519],{},"Technology",[1287,3521,3522,3533,3544,3555],{},[1271,3523,3524,3526,3528,3531],{},[1292,3525,1294],{},[1292,3527,1297],{},[1292,3529,3530],{},"Keyword candidates (BM25)",[1292,3532,1303],{},[1271,3534,3535,3537,3539,3542],{},[1292,3536,1308],{},[1292,3538,1311],{},[1292,3540,3541],{},"Token intersection",[1292,3543,1317],{},[1271,3545,3546,3548,3550,3553],{},[1292,3547,1322],{},[1292,3549,1311],{},[1292,3551,3552],{},"Compositional algebra (probe\u002Frelated\u002Freason)",[1292,3554,1330],{},[1271,3556,3557,3559,3561,3564],{},[1292,3558,1335],{},[1292,3560,1297],{},[1292,3562,3563],{},"Semantic similarity",[1292,3565,1343],{},[15,3567,3568,3569,3571,3572,1354],{},"Final score: ",[73,3570,1349],{},", where ",[73,3573,1353],{},[15,3575,3576],{},"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.",[22,3578,3580],{"id":3579},"search-pipeline","Search Pipeline",[66,3582,3585],{"className":3583,"code":3584,"language":1367},[1365],"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",[73,3586,3584],{"__ignoreMap":71},[15,3588,3589],{},"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.",[22,3591,3593],{"id":3592},"why-not-off-the-shelf-solutions","Why Not Off-the-Shelf Solutions",[137,3595,1380],{"id":1379},[15,3597,3598],{},"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.",[137,3600,1387],{"id":1386},[15,3602,3603],{},"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.",[137,3605,1394],{"id":1393},[15,3607,3608],{},"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?",[137,3610,3612],{"id":3611},"custom-solution","Custom Solution",[15,3614,3615],{},"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.",[22,3617,3619],{"id":3618},"model-selection-mpnet-vs-minilm","Model Selection: mpnet vs MiniLM",[15,3621,3622],{},"I tested two multilingual fastembed models:",[1265,3624,3625,3635],{},[1268,3626,3627],{},[1271,3628,3629,3631,3633],{},[1274,3630],{},[1274,3632,1422],{},[1274,3634,1425],{},[1287,3636,3637,3646,3657,3668,3679,3690,3699],{},[1271,3638,3639,3642,3644],{},[1292,3640,3641],{},"Dimensions",[1292,3643,1435],{},[1292,3645,1438],{},[1271,3647,3648,3651,3654],{},[1292,3649,3650],{},"RSS (loaded)",[1292,3652,3653],{},"1440 MB",[1292,3655,3656],{},"680 MB",[1271,3658,3659,3662,3665],{},[1292,3660,3661],{},"RSS (residual after unload)",[1292,3663,3664],{},"693 MB",[1292,3666,3667],{},"481 MB",[1271,3669,3670,3673,3676],{},[1292,3671,3672],{},"Embedding time",[1292,3674,3675],{},"65 ms",[1292,3677,3678],{},"46 ms",[1271,3680,3681,3684,3687],{},[1292,3682,3683],{},"Reload time after unload",[1292,3685,3686],{},"20–25 s",[1292,3688,3689],{},"1.33 s",[1271,3691,3692,3695,3697],{},[1292,3693,3694],{},"sim(\"compact format\" ↔ \"concise messages\")",[1292,3696,1490],{},[1292,3698,1493],{},[1271,3700,3701,3704,3706],{},[1292,3702,3703],{},"sim(\"compact format\" ↔ \"weather for a walk\")",[1292,3705,1501],{},[1292,3707,1504],{},[15,3709,3710],{},"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.",[15,3712,3713],{},"MiniLM scores 0.511 for semantically similar phrases and 0.008 for unrelated ones — sufficient separation. Not ideal (mpnet is ~20% better), but functional.",[15,3715,3716,3718,3719,3721],{},[478,3717,1515],{}," refers to ONNX Runtime, which doesn't release memory pools even after ",[73,3720,1519],{},". 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.",[22,3723,3725],{"id":3724},"lazy-loading-and-model-lifecycle","Lazy-Loading and Model Lifecycle",[66,3727,3729],{"className":1527,"code":3728,"language":1529,"meta":71,"style":71},"_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",[73,3730,3731,3739,3757,3761,3775,3780,3794,3800,3806,3812,3820,3828,3836,3842,3846,3854,3859,3865,3877,3887,3901,3907,3911,3937,3947,3973],{"__ignoreMap":71},[76,3732,3733,3735,3737],{"class":78,"line":79},[76,3734,1536],{"class":107},[76,3736,112],{"class":111},[76,3738,1541],{"class":189},[76,3740,3741,3743,3745,3747,3749,3751,3753,3755],{"class":78,"line":118},[76,3742,1546],{"class":107},[76,3744,1550],{"class":1549},[76,3746,1554],{"class":1553},[76,3748,1557],{"class":1549},[76,3750,1561],{"class":1560},[76,3752,1564],{"class":1549},[76,3754,1567],{"class":111},[76,3756,1541],{"class":189},[76,3758,3759],{"class":78,"line":349},[76,3760,346],{"emptyLinePlaceholder":345},[76,3762,3763,3765,3767,3769,3771,3773],{"class":78,"line":356},[76,3764,1578],{"class":103},[76,3766,1581],{"class":82},[76,3768,1584],{"class":1549},[76,3770,1587],{"class":1549},[76,3772,1590],{"class":1560},[76,3774,1593],{"class":1549},[76,3776,3777],{"class":78,"line":365},[76,3778,3779],{"class":86},"    \"\"\"Check without loading the model.\"\"\"\n",[76,3781,3782,3784,3786,3788,3790,3792],{"class":78,"line":370},[76,3783,1603],{"class":103},[76,3785,1606],{"class":107},[76,3787,1609],{"class":103},[76,3789,1612],{"class":103},[76,3791,1615],{"class":189},[76,3793,1593],{"class":1549},[76,3795,3796,3798],{"class":78,"line":376},[76,3797,1622],{"class":103},[76,3799,1625],{"class":107},[76,3801,3802,3804],{"class":78,"line":1628},[76,3803,1631],{"class":103},[76,3805,1593],{"class":1549},[76,3807,3808,3810],{"class":78,"line":1636},[76,3809,1639],{"class":103},[76,3811,1642],{"class":107},[76,3813,3814,3816,3818],{"class":78,"line":1645},[76,3815,1648],{"class":107},[76,3817,112],{"class":111},[76,3819,1653],{"class":189},[76,3821,3822,3824,3826],{"class":78,"line":1656},[76,3823,1659],{"class":103},[76,3825,1663],{"class":1662},[76,3827,1593],{"class":1549},[76,3829,3830,3832,3834],{"class":78,"line":1668},[76,3831,1648],{"class":107},[76,3833,112],{"class":111},[76,3835,1675],{"class":189},[76,3837,3838,3840],{"class":78,"line":1678},[76,3839,1681],{"class":103},[76,3841,1625],{"class":107},[76,3843,3844],{"class":78,"line":1686},[76,3845,346],{"emptyLinePlaceholder":345},[76,3847,3848,3850,3852],{"class":78,"line":1691},[76,3849,1578],{"class":103},[76,3851,1696],{"class":82},[76,3853,1699],{"class":1549},[76,3855,3856],{"class":78,"line":1702},[76,3857,3858],{"class":86},"    \"\"\"Lazy-load on first embed_text() call.\"\"\"\n",[76,3860,3861,3863],{"class":78,"line":1708},[76,3862,1711],{"class":103},[76,3864,1714],{"class":107},[76,3866,3867,3869,3871,3873,3875],{"class":78,"line":1717},[76,3868,1603],{"class":103},[76,3870,1722],{"class":107},[76,3872,1609],{"class":103},[76,3874,1615],{"class":189},[76,3876,1593],{"class":1549},[76,3878,3879,3881,3883,3885],{"class":78,"line":1731},[76,3880,1734],{"class":103},[76,3882,1737],{"class":107},[76,3884,1740],{"class":103},[76,3886,1743],{"class":107},[76,3888,3889,3891,3893,3895,3897,3899],{"class":78,"line":1746},[76,3890,1749],{"class":107},[76,3892,112],{"class":111},[76,3894,1755],{"class":1754},[76,3896,1758],{"class":1549},[76,3898,1761],{"class":86},[76,3900,1764],{"class":1549},[76,3902,3903,3905],{"class":78,"line":1767},[76,3904,1681],{"class":103},[76,3906,1714],{"class":107},[76,3908,3909],{"class":78,"line":1774},[76,3910,346],{"emptyLinePlaceholder":345},[76,3912,3913,3915,3917,3919,3921,3923,3925,3927,3929,3931,3933,3935],{"class":78,"line":1779},[76,3914,1578],{"class":103},[76,3916,1784],{"class":82},[76,3918,1758],{"class":1549},[76,3920,1367],{"class":1553},[76,3922,1550],{"class":1549},[76,3924,1793],{"class":1560},[76,3926,1796],{"class":1549},[76,3928,1587],{"class":1549},[76,3930,1801],{"class":107},[76,3932,1354],{"class":1549},[76,3934,1806],{"class":107},[76,3936,1593],{"class":1549},[76,3938,3939,3941,3943,3945],{"class":78,"line":1811},[76,3940,1814],{"class":107},[76,3942,112],{"class":111},[76,3944,1696],{"class":1754},[76,3946,1821],{"class":1549},[76,3948,3949,3951,3953,3955,3957,3959,3961,3963,3965,3967,3969,3971],{"class":78,"line":1824},[76,3950,1827],{"class":107},[76,3952,112],{"class":111},[76,3954,543],{"class":1560},[76,3956,1758],{"class":1549},[76,3958,1836],{"class":107},[76,3960,1354],{"class":1549},[76,3962,1841],{"class":1754},[76,3964,1844],{"class":1549},[76,3966,1367],{"class":107},[76,3968,1849],{"class":1549},[76,3970,1852],{"class":189},[76,3972,1855],{"class":1549},[76,3974,3975,3977,3979,3981,3983,3985,3987,3989,3991,3993,3995,3997],{"class":78,"line":1858},[76,3976,1681],{"class":103},[76,3978,1863],{"class":107},[76,3980,1866],{"class":111},[76,3982,1801],{"class":107},[76,3984,1354],{"class":1549},[76,3986,1873],{"class":107},[76,3988,1354],{"class":1549},[76,3990,1878],{"class":1754},[76,3992,1758],{"class":1549},[76,3994,1883],{"class":107},[76,3996,1796],{"class":1549},[76,3998,1888],{"class":352},[15,4000,4001,4002,4004],{},"First ",[73,4003,1894],{}," call takes ~1.3 s (loading the ONNX model). All subsequent calls take ~46 ms. The model stays in memory until shutdown.",[137,4006,4008],{"id":4007},"unloading-on-shutdown","Unloading on Shutdown",[66,4010,4012],{"className":1527,"code":4011,"language":1529,"meta":71,"style":71},"# 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",[73,4013,4014,4019,4029,4033,4041,4053,4063],{"__ignoreMap":71},[76,4015,4016],{"class":78,"line":79},[76,4017,4018],{"class":352},"# In the plugin's __init__.py, on shutdown hook:\n",[76,4020,4021,4023,4025,4027],{"class":78,"line":118},[76,4022,1740],{"class":103},[76,4024,1916],{"class":107},[76,4026,1919],{"class":1549},[76,4028,1922],{"class":107},[76,4030,4031],{"class":78,"line":349},[76,4032,346],{"emptyLinePlaceholder":345},[76,4034,4035,4037,4039],{"class":78,"line":356},[76,4036,1578],{"class":103},[76,4038,1933],{"class":82},[76,4040,1699],{"class":1549},[76,4042,4043,4045,4047,4049,4051],{"class":78,"line":365},[76,4044,1940],{"class":107},[76,4046,1354],{"class":1549},[76,4048,1536],{"class":107},[76,4050,112],{"class":111},[76,4052,1541],{"class":189},[76,4054,4055,4057,4059,4061],{"class":78,"line":370},[76,4056,1953],{"class":107},[76,4058,1354],{"class":1549},[76,4060,1958],{"class":1754},[76,4062,1821],{"class":1549},[76,4064,4065],{"class":78,"line":376},[76,4066,4067],{"class":352},"    # Frees ~200 MB (model weights), but ONNX residual (481 MB) remains\n",[15,4069,4070],{},"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.",[22,4072,4074],{"id":4073},"storage-two-vectors-per-fact","Storage: Two Vectors Per Fact",[15,4076,4077],{},"Each fact in SQLite stores two vectors:",[66,4079,4080],{"className":1978,"code":1979,"language":1980,"meta":71,"style":71},[73,4081,4082,4092,4102,4112,4124,4136,4152,4164,4176,4182,4188,4196,4202],{"__ignoreMap":71},[76,4083,4084,4086,4088,4090],{"class":78,"line":79},[76,4085,1987],{"class":103},[76,4087,1990],{"class":103},[76,4089,1993],{"class":82},[76,4091,1996],{"class":107},[76,4093,4094,4096,4098,4100],{"class":78,"line":118},[76,4095,2001],{"class":107},[76,4097,2004],{"class":103},[76,4099,2007],{"class":103},[76,4101,2010],{"class":107},[76,4103,4104,4106,4108,4110],{"class":78,"line":349},[76,4105,2015],{"class":107},[76,4107,2018],{"class":103},[76,4109,2021],{"class":103},[76,4111,2010],{"class":107},[76,4113,4114,4116,4118,4120,4122],{"class":78,"line":356},[76,4115,2028],{"class":107},[76,4117,2018],{"class":103},[76,4119,2033],{"class":103},[76,4121,2036],{"class":86},[76,4123,2010],{"class":107},[76,4125,4126,4128,4130,4132,4134],{"class":78,"line":365},[76,4127,2043],{"class":107},[76,4129,2018],{"class":103},[76,4131,2033],{"class":103},[76,4133,2050],{"class":86},[76,4135,2010],{"class":107},[76,4137,4138,4140,4142,4144,4146,4148,4150],{"class":78,"line":370},[76,4139,2057],{"class":107},[76,4141,2060],{"class":103},[76,4143,2033],{"class":103},[76,4145,2065],{"class":189},[76,4147,1354],{"class":107},[76,4149,2070],{"class":189},[76,4151,2010],{"class":107},[76,4153,4154,4156,4158,4160,4162],{"class":78,"line":376},[76,4155,2077],{"class":107},[76,4157,2004],{"class":103},[76,4159,2033],{"class":103},[76,4161,2065],{"class":189},[76,4163,2010],{"class":107},[76,4165,4166,4168,4170,4172,4174],{"class":78,"line":1628},[76,4167,2090],{"class":107},[76,4169,2004],{"class":103},[76,4171,2033],{"class":103},[76,4173,2065],{"class":189},[76,4175,2010],{"class":107},[76,4177,4178,4180],{"class":78,"line":1636},[76,4179,2103],{"class":107},[76,4181,2106],{"class":352},[76,4183,4184,4186],{"class":78,"line":1645},[76,4185,2111],{"class":107},[76,4187,2114],{"class":352},[76,4189,4190,4192,4194],{"class":78,"line":1656},[76,4191,2119],{"class":107},[76,4193,2018],{"class":103},[76,4195,2010],{"class":107},[76,4197,4198,4200],{"class":78,"line":1668},[76,4199,2128],{"class":107},[76,4201,2131],{"class":103},[76,4203,4204],{"class":78,"line":1678},[76,4205,2136],{"class":107},[15,4207,4208,4211],{},[478,4209,4210],{},"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.",[15,4213,4214,4217],{},[478,4215,4216],{},"Semantic vector"," (1.5 KB per fact) — fastembed output. Normalized unit vector for cosine similarity. Used in hybrid scoring.",[15,4219,4220],{},"Total: ~9.7 KB per fact. For 1000 facts — ~10 MB. Negligible.",[22,4222,4224],{"id":4223},"hrr-compositional-algebra-without-neural-networks","HRR: Compositional Algebra Without Neural Networks",[15,4226,4227],{},"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.",[66,4229,4231],{"className":1527,"code":4230,"language":1529,"meta":71,"style":71},"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",[73,4232,4233,4271,4276,4302,4344,4349,4379,4415,4419,4435,4440,4490,4494,4506,4511,4537],{"__ignoreMap":71},[76,4234,4235,4237,4239,4241,4243,4245,4247,4249,4251,4253,4255,4257,4259,4261,4263,4265,4267,4269],{"class":78,"line":79},[76,4236,1578],{"class":103},[76,4238,2170],{"class":82},[76,4240,1758],{"class":1549},[76,4242,2175],{"class":1553},[76,4244,1550],{"class":1549},[76,4246,1793],{"class":1560},[76,4248,1919],{"class":1549},[76,4250,2184],{"class":1553},[76,4252,1550],{"class":1549},[76,4254,2189],{"class":1560},[76,4256,1567],{"class":111},[76,4258,2194],{"class":189},[76,4260,1796],{"class":1549},[76,4262,1587],{"class":1549},[76,4264,1801],{"class":107},[76,4266,1354],{"class":1549},[76,4268,1806],{"class":107},[76,4270,1593],{"class":1549},[76,4272,4273],{"class":78,"line":118},[76,4274,4275],{"class":86},"    \"\"\"Token → unit vector in phase space.\"\"\"\n",[76,4277,4278,4280,4282,4284,4286,4288,4290,4292,4294,4296,4298,4300],{"class":78,"line":349},[76,4279,2216],{"class":107},[76,4281,112],{"class":111},[76,4283,2221],{"class":107},[76,4285,1354],{"class":1549},[76,4287,2226],{"class":1754},[76,4289,1758],{"class":1549},[76,4291,2175],{"class":107},[76,4293,1354],{"class":1549},[76,4295,2235],{"class":1754},[76,4297,2238],{"class":1549},[76,4299,2241],{"class":1754},[76,4301,1821],{"class":1549},[76,4303,4304,4306,4308,4310,4312,4314,4316,4318,4320,4322,4324,4326,4328,4330,4332,4334,4336,4338,4340,4342],{"class":78,"line":356},[76,4305,2248],{"class":107},[76,4307,112],{"class":111},[76,4309,1801],{"class":107},[76,4311,1354],{"class":1549},[76,4313,2257],{"class":1754},[76,4315,1758],{"class":1549},[76,4317,2262],{"class":107},[76,4319,1919],{"class":1549},[76,4321,2268],{"class":2267},[76,4323,112],{"class":111},[76,4325,2273],{"class":107},[76,4327,1354],{"class":1549},[76,4329,2278],{"class":107},[76,4331,2281],{"class":1549},[76,4333,2284],{"class":1754},[76,4335,1758],{"class":1549},[76,4337,2273],{"class":107},[76,4339,1354],{"class":1549},[76,4341,2293],{"class":107},[76,4343,1764],{"class":1549},[76,4345,4346],{"class":78,"line":365},[76,4347,4348],{"class":352},"    # Phase code: cos + j*sin → unit vector in complex space\n",[76,4350,4351,4353,4355,4357,4359,4361,4363,4365,4367,4369,4371,4373,4375,4377],{"class":78,"line":370},[76,4352,2305],{"class":107},[76,4354,112],{"class":111},[76,4356,2310],{"class":1553},[76,4358,2313],{"class":1549},[76,4360,2316],{"class":1553},[76,4362,1564],{"class":1549},[76,4364,2321],{"class":111},[76,4366,2324],{"class":189},[76,4368,2327],{"class":111},[76,4370,2330],{"class":189},[76,4372,2327],{"class":111},[76,4374,1801],{"class":107},[76,4376,1354],{"class":1549},[76,4378,2339],{"class":107},[76,4380,4381,4383,4385,4387,4389,4391,4393,4395,4397,4399,4401,4403,4405,4407,4409,4411,4413],{"class":78,"line":376},[76,4382,1681],{"class":103},[76,4384,1801],{"class":107},[76,4386,1354],{"class":1549},[76,4388,2350],{"class":1754},[76,4390,1758],{"class":1549},[76,4392,2355],{"class":107},[76,4394,1796],{"class":1549},[76,4396,2360],{"class":111},[76,4398,2363],{"class":189},[76,4400,2366],{"class":103},[76,4402,2327],{"class":111},[76,4404,1801],{"class":107},[76,4406,1354],{"class":1549},[76,4408,2375],{"class":1754},[76,4410,1758],{"class":1549},[76,4412,2355],{"class":107},[76,4414,1764],{"class":1549},[76,4416,4417],{"class":78,"line":1628},[76,4418,346],{"emptyLinePlaceholder":345},[76,4420,4421,4423,4425,4427,4429,4431,4433],{"class":78,"line":1636},[76,4422,1578],{"class":103},[76,4424,2392],{"class":82},[76,4426,1758],{"class":1549},[76,4428,58],{"class":1553},[76,4430,1919],{"class":1549},[76,4432,2401],{"class":1553},[76,4434,2404],{"class":1549},[76,4436,4437],{"class":78,"line":1645},[76,4438,4439],{"class":86},"    \"\"\"Binding: circular convolution in frequency domain.\"\"\"\n",[76,4441,4442,4444,4446,4448,4450,4452,4454,4456,4458,4460,4462,4464,4466,4468,4470,4472,4474,4476,4478,4480,4482,4484,4486,4488],{"class":78,"line":1656},[76,4443,1681],{"class":103},[76,4445,1801],{"class":107},[76,4447,1354],{"class":1549},[76,4449,2420],{"class":107},[76,4451,1354],{"class":1549},[76,4453,2425],{"class":1754},[76,4455,1758],{"class":1549},[76,4457,2273],{"class":107},[76,4459,1354],{"class":1549},[76,4461,2420],{"class":107},[76,4463,1354],{"class":1549},[76,4465,2420],{"class":1754},[76,4467,1758],{"class":1549},[76,4469,58],{"class":107},[76,4471,1796],{"class":1549},[76,4473,2327],{"class":111},[76,4475,1801],{"class":107},[76,4477,1354],{"class":1549},[76,4479,2420],{"class":107},[76,4481,1354],{"class":1549},[76,4483,2420],{"class":1754},[76,4485,1758],{"class":1549},[76,4487,2460],{"class":107},[76,4489,2463],{"class":1549},[76,4491,4492],{"class":78,"line":1668},[76,4493,346],{"emptyLinePlaceholder":345},[76,4495,4496,4498,4500,4502,4504],{"class":78,"line":1678},[76,4497,1578],{"class":103},[76,4499,2474],{"class":82},[76,4501,1758],{"class":1549},[76,4503,2479],{"class":1553},[76,4505,2404],{"class":1549},[76,4507,4508],{"class":78,"line":1686},[76,4509,4510],{"class":86},"    \"\"\"Bundling: element-wise sum + normalization.\"\"\"\n",[76,4512,4513,4515,4517,4519,4521,4523,4525,4527,4529,4531,4533,4535],{"class":78,"line":1691},[76,4514,2491],{"class":107},[76,4516,112],{"class":111},[76,4518,1801],{"class":107},[76,4520,1354],{"class":1549},[76,4522,2500],{"class":1754},[76,4524,1758],{"class":1549},[76,4526,2479],{"class":107},[76,4528,1919],{"class":1549},[76,4530,2509],{"class":2267},[76,4532,112],{"class":111},[76,4534,1852],{"class":189},[76,4536,1764],{"class":1549},[76,4538,4539,4541,4543,4545,4547,4549,4551,4553,4555,4557,4559],{"class":78,"line":1702},[76,4540,1681],{"class":103},[76,4542,2522],{"class":107},[76,4544,1866],{"class":111},[76,4546,1801],{"class":107},[76,4548,1354],{"class":1549},[76,4550,1873],{"class":107},[76,4552,1354],{"class":1549},[76,4554,1878],{"class":1754},[76,4556,1758],{"class":1549},[76,4558,2539],{"class":107},[76,4560,1764],{"class":1549},[15,4562,4563,4564,4567],{},"Why bother when you have fastembed? Because HRR supports ",[478,4565,4566],{},"algebraic operations"," that vector models can't do:",[2550,4569,4570,4575,4580],{},[33,4571,4572,4574],{},[478,4573,2556],{}," — \"give me all facts bound to entity X.\" Bind\u002Funbind with the entity atom.",[33,4576,4577,4579],{},[478,4578,2562],{}," — \"what structurally neighbors X.\" Via HRR similarity.",[33,4581,4582,4586,4587,1354],{},[478,4583,2568,4584,1796],{},[76,4585,2571],{}," — \"what do multiple entities have in common.\" Multi-entity JOIN, ",[73,4588,2575],{},[15,4590,4591],{},"The semantic model gives \"similarity in meaning,\" HRR gives \"connectedness by structure.\" These are different axes.",[22,4593,4595],{"id":4594},"jaccard-a-cheap-filter","Jaccard: A Cheap Filter",[15,4597,4598],{},"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.",[66,4600,4601],{"className":1527,"code":2588,"language":1529,"meta":71,"style":71},[73,4602,4603,4633,4649,4655],{"__ignoreMap":71},[76,4604,4605,4607,4609,4611,4613,4615,4617,4619,4621,4623,4625,4627,4629,4631],{"class":78,"line":79},[76,4606,1578],{"class":103},[76,4608,2597],{"class":82},[76,4610,1758],{"class":1549},[76,4612,2602],{"class":1553},[76,4614,1550],{"class":1549},[76,4616,2607],{"class":1560},[76,4618,1919],{"class":1549},[76,4620,2612],{"class":1553},[76,4622,1550],{"class":1549},[76,4624,2607],{"class":1560},[76,4626,1796],{"class":1549},[76,4628,1587],{"class":1549},[76,4630,2623],{"class":1560},[76,4632,1593],{"class":1549},[76,4634,4635,4637,4639,4641,4643,4645,4647],{"class":78,"line":118},[76,4636,1603],{"class":103},[76,4638,1612],{"class":103},[76,4640,2634],{"class":107},[76,4642,2637],{"class":103},[76,4644,1612],{"class":103},[76,4646,2612],{"class":107},[76,4648,1593],{"class":1549},[76,4650,4651,4653],{"class":78,"line":349},[76,4652,1622],{"class":103},[76,4654,2650],{"class":189},[76,4656,4657,4659,4661,4663,4665,4667,4669,4671,4673,4675,4677,4679,4681,4683],{"class":78,"line":356},[76,4658,1681],{"class":103},[76,4660,2657],{"class":1662},[76,4662,1758],{"class":1549},[76,4664,2662],{"class":107},[76,4666,2665],{"class":111},[76,4668,2612],{"class":107},[76,4670,1796],{"class":1549},[76,4672,2321],{"class":111},[76,4674,2657],{"class":1662},[76,4676,1758],{"class":1549},[76,4678,2662],{"class":107},[76,4680,2680],{"class":111},[76,4682,2612],{"class":107},[76,4684,1764],{"class":1549},[15,4686,4687],{},"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.",[22,4689,4691],{"id":4690},"fts5-full-text-indexing","FTS5: Full-Text Indexing",[15,4693,4694],{},"SQLite FTS5 is built-in full-text indexing. AND semantics: all query words must appear in the fact. Strict, but predictable.",[66,4696,4698],{"className":1978,"code":4697,"language":1980,"meta":71,"style":71},"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",[73,4699,4700,4722,4726,4731,4750],{"__ignoreMap":71},[76,4701,4702,4704,4706,4708,4710,4712,4714,4716,4718,4720],{"class":78,"line":79},[76,4703,1987],{"class":103},[76,4705,2706],{"class":107},[76,4707,2709],{"class":103},[76,4709,2712],{"class":107},[76,4711,2715],{"class":103},[76,4713,2718],{"class":107},[76,4715,112],{"class":111},[76,4717,2723],{"class":107},[76,4719,112],{"class":111},[76,4721,2728],{"class":107},[76,4723,4724],{"class":78,"line":118},[76,4725,346],{"emptyLinePlaceholder":345},[76,4727,4728],{"class":78,"line":349},[76,4729,4730],{"class":352},"-- Search:\n",[76,4732,4733,4735,4737,4739,4741,4743,4745,4747],{"class":78,"line":356},[76,4734,2742],{"class":103},[76,4736,2745],{"class":107},[76,4738,2748],{"class":103},[76,4740,2712],{"class":107},[76,4742,2753],{"class":103},[76,4744,2712],{"class":107},[76,4746,2758],{"class":103},[76,4748,4749],{"class":86}," 'deploy nginx'\n",[76,4751,4752,4754,4756,4758,4760],{"class":78,"line":365},[76,4753,2766],{"class":103},[76,4755,2769],{"class":107},[76,4757,2772],{"class":103},[76,4759,2775],{"class":189},[76,4761,2778],{"class":107},[15,4763,4764],{},"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.",[22,4766,4768],{"id":4767},"resource-usage-on-a-potato-vps","Resource Usage on a Potato VPS",[15,4770,4771],{},"Typical memory footprint with the agent running:",[1265,4773,4774,4783],{},[1268,4775,4776],{},[1271,4777,4778,4781],{},[1274,4779,4780],{},"Component",[1274,4782,2800],{},[1287,4784,4785,4792,4800,4807,4814],{},[1271,4786,4787,4789],{},[1292,4788,2807],{},[1292,4790,4791],{},"~380 MB",[1271,4793,4794,4797],{},[1292,4795,4796],{},"fastembed (loaded)",[1292,4798,4799],{},"+300 MB",[1271,4801,4802,4804],{},[1292,4803,2823],{},[1292,4805,4806],{},"(included above)",[1271,4808,4809,4811],{},[1292,4810,2831],{},[1292,4812,4813],{},"~5 MB",[1271,4815,4816,4819],{},[1292,4817,4818],{},"Total",[1292,4820,4821],{},"~680 MB",[15,4823,4824],{},"Plenty left for the OS, swap, and other processes. Comfortable.",[15,4826,4827,4828,4830],{},"On shutdown, the model is unloaded and RSS drops to ~620 MB. ONNX residual isn't freed — that's the cost of a single ",[73,4829,2851],{}," per process lifetime.",[22,4832,4833],{"id":2855},"Weight Auto-Redistribution",[15,4835,4836],{},"If fastembed is unavailable (not installed, or numpy missing), weights are redistributed automatically:",[66,4838,4840],{"className":1527,"code":4839,"language":1529,"meta":71,"style":71},"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",[73,4841,4842,4854,4868,4873,4885,4897,4909,4921,4931,4936,4948,4960,4972],{"__ignoreMap":71},[76,4843,4844,4846,4848,4850,4852],{"class":78,"line":79},[76,4845,1578],{"class":103},[76,4847,2871],{"class":82},[76,4849,1758],{"class":1549},[76,4851,2877],{"class":2876},[76,4853,2404],{"class":1549},[76,4855,4856,4858,4860,4862,4864,4866],{"class":78,"line":118},[76,4857,1603],{"class":103},[76,4859,1612],{"class":103},[76,4861,1916],{"class":107},[76,4863,1354],{"class":1549},[76,4865,2892],{"class":1754},[76,4867,1699],{"class":1549},[76,4869,4870],{"class":78,"line":349},[76,4871,4872],{"class":352},"        # Semantic unavailable: 0.3 → FTS +0.15, Jaccard +0.1, HRR +0.05\n",[76,4874,4875,4877,4879,4881,4883],{"class":78,"line":356},[76,4876,2905],{"class":2904},[76,4878,1354],{"class":1549},[76,4880,2910],{"class":107},[76,4882,112],{"class":111},[76,4884,2915],{"class":189},[76,4886,4887,4889,4891,4893,4895],{"class":78,"line":365},[76,4888,2905],{"class":2904},[76,4890,1354],{"class":1549},[76,4892,2924],{"class":107},[76,4894,112],{"class":111},[76,4896,2929],{"class":189},[76,4898,4899,4901,4903,4905,4907],{"class":78,"line":370},[76,4900,2905],{"class":2904},[76,4902,1354],{"class":1549},[76,4904,2938],{"class":107},[76,4906,112],{"class":111},[76,4908,2943],{"class":189},[76,4910,4911,4913,4915,4917,4919],{"class":78,"line":376},[76,4912,2905],{"class":2904},[76,4914,1354],{"class":1549},[76,4916,2952],{"class":107},[76,4918,112],{"class":111},[76,4920,2650],{"class":189},[76,4922,4923,4925,4927,4929],{"class":78,"line":1628},[76,4924,2961],{"class":103},[76,4926,1612],{"class":103},[76,4928,2967],{"class":2966},[76,4930,1593],{"class":1549},[76,4932,4933],{"class":78,"line":1636},[76,4934,4935],{"class":352},"        # HRR unavailable: 0.2 → FTS +0.1, Semantic +0.1\n",[76,4937,4938,4940,4942,4944,4946],{"class":78,"line":1645},[76,4939,2905],{"class":2904},[76,4941,1354],{"class":1549},[76,4943,2910],{"class":107},[76,4945,112],{"class":111},[76,4947,2987],{"class":189},[76,4949,4950,4952,4954,4956,4958],{"class":78,"line":1656},[76,4951,2905],{"class":2904},[76,4953,1354],{"class":1549},[76,4955,2924],{"class":107},[76,4957,112],{"class":111},[76,4959,3000],{"class":189},[76,4961,4962,4964,4966,4968,4970],{"class":78,"line":1668},[76,4963,2905],{"class":2904},[76,4965,1354],{"class":1549},[76,4967,2938],{"class":107},[76,4969,112],{"class":111},[76,4971,2650],{"class":189},[76,4973,4974,4976,4978,4980,4982],{"class":78,"line":1678},[76,4975,2905],{"class":2904},[76,4977,1354],{"class":1549},[76,4979,2952],{"class":107},[76,4981,112],{"class":111},[76,4983,2987],{"class":189},[15,4985,4986],{},"Graceful degradation: the system works without embeddings (FTS + Jaccard) and without HRR (FTS + Jaccard + Semantic). But the full quartet is optimal.",[22,4988,4990],{"id":4989},"practical-pitfalls","Practical Pitfalls",[137,4992,4994],{"id":4993},"_1-fts5-and-is-too-strict","1. FTS5 AND Is Too Strict",[15,4996,4997],{},"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.",[15,4999,5000,5003],{},[478,5001,5002],{},"Solution:"," Don't rely on FTS5 as the sole channel. Always keep semantic enabled.",[137,5005,5007],{"id":5006},"_2-missing-semantic-vectors-after-update","2. Missing Semantic Vectors After Update",[15,5009,5010,5011,5013],{},"The agent was updated, but the old process is still running. New facts are written without ",[73,5012,3054],{},". Symptom: Russian queries return empty results.",[66,5015,5016],{"className":1978,"code":3058,"language":1980,"meta":71,"style":71},[73,5017,5018],{"__ignoreMap":71},[76,5019,5020,5022,5024,5026,5028,5030,5032,5034,5036,5038,5040,5042],{"class":78,"line":79},[76,5021,2742],{"class":103},[76,5023,3068],{"class":3067},[76,5025,1758],{"class":107},[76,5027,3073],{"class":111},[76,5029,3076],{"class":107},[76,5031,2748],{"class":103},[76,5033,3081],{"class":107},[76,5035,2753],{"class":103},[76,5037,3086],{"class":107},[76,5039,3089],{"class":103},[76,5041,3092],{"class":103},[76,5043,2778],{"class":107},[15,5045,5046],{},"If > 0 — run a backfill:",[66,5048,5050],{"className":1527,"code":5049,"language":1529,"meta":71,"style":71},"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",[73,5051,5052,5062,5066,5087,5109,5113,5129,5147,5161,5181,5185],{"__ignoreMap":71},[76,5053,5054,5056,5058,5060],{"class":78,"line":79},[76,5055,1740],{"class":103},[76,5057,1916],{"class":107},[76,5059,1919],{"class":1549},[76,5061,3113],{"class":107},[76,5063,5064],{"class":78,"line":118},[76,5065,346],{"emptyLinePlaceholder":345},[76,5067,5068,5070,5072,5074,5076,5078,5080,5082,5084],{"class":78,"line":349},[76,5069,3122],{"class":107},[76,5071,112],{"class":111},[76,5073,3127],{"class":107},[76,5075,1354],{"class":1549},[76,5077,3132],{"class":1754},[76,5079,1758],{"class":1549},[76,5081,3137],{"class":107},[76,5083,1796],{"class":1549},[76,5085,5086],{"class":352},"  # path to your database\n",[76,5088,5089,5091,5093,5095,5097,5099,5101,5103,5105,5107],{"class":78,"line":356},[76,5090,3147],{"class":107},[76,5092,112],{"class":111},[76,5094,3152],{"class":107},[76,5096,1354],{"class":1549},[76,5098,3157],{"class":1754},[76,5100,1758],{"class":1549},[76,5102,3162],{"class":86},[76,5104,2281],{"class":1549},[76,5106,3167],{"class":1754},[76,5108,1821],{"class":1549},[76,5110,5111],{"class":78,"line":365},[76,5112,346],{"emptyLinePlaceholder":345},[76,5114,5115,5117,5119,5121,5123,5125,5127],{"class":78,"line":370},[76,5116,3178],{"class":103},[76,5118,3181],{"class":107},[76,5120,1919],{"class":1549},[76,5122,3186],{"class":107},[76,5124,3189],{"class":103},[76,5126,3192],{"class":107},[76,5128,1593],{"class":1549},[76,5130,5131,5133,5135,5137,5139,5141,5143,5145],{"class":78,"line":376},[76,5132,1827],{"class":107},[76,5134,112],{"class":111},[76,5136,1916],{"class":107},[76,5138,1354],{"class":1549},[76,5140,3207],{"class":1754},[76,5142,1758],{"class":1549},[76,5144,3212],{"class":107},[76,5146,1764],{"class":1549},[76,5148,5149,5151,5153,5155,5157,5159],{"class":78,"line":1628},[76,5150,3219],{"class":107},[76,5152,1354],{"class":1549},[76,5154,3157],{"class":1754},[76,5156,1758],{"class":1549},[76,5158,3228],{"class":86},[76,5160,2010],{"class":1549},[76,5162,5163,5165,5167,5169,5171,5173,5175,5177,5179],{"class":78,"line":1636},[76,5164,3235],{"class":1549},[76,5166,3238],{"class":107},[76,5168,1354],{"class":1549},[76,5170,3243],{"class":1754},[76,5172,1758],{"class":1549},[76,5174,1883],{"class":107},[76,5176,3250],{"class":1549},[76,5178,3181],{"class":107},[76,5180,2463],{"class":1549},[76,5182,5183],{"class":78,"line":1645},[76,5184,346],{"emptyLinePlaceholder":345},[76,5186,5187,5189,5191,5193],{"class":78,"line":1656},[76,5188,3263],{"class":107},[76,5190,1354],{"class":1549},[76,5192,3268],{"class":1754},[76,5194,1821],{"class":1549},[137,5196,5197],{"id":3273},"3. fastembed Pooling Change",[15,5199,5200],{},"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.",[137,5202,5204],{"id":5203},"_4-onnx-memory-leak-its-not-a-leak","4. ONNX Memory Leak (It's Not a Leak)",[15,5206,5207,5209],{},[73,5208,1519],{}," 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.",[137,5211,5212],{"id":3289},"5. RSS ≠ Used Memory",[15,5214,5215,5217],{},[73,5216,3295],{}," shows the peak, not current consumption. For accurate measurement:",[66,5219,5220],{"className":1527,"code":3299,"language":1529,"meta":71,"style":71},[73,5221,5222,5230,5248,5274],{"__ignoreMap":71},[76,5223,5224,5226,5228],{"class":78,"line":79},[76,5225,1578],{"class":103},[76,5227,3308],{"class":82},[76,5229,1699],{"class":1549},[76,5231,5232,5234,5236,5238,5240,5242,5244,5246],{"class":78,"line":118},[76,5233,3315],{"class":103},[76,5235,3318],{"class":1662},[76,5237,1758],{"class":1549},[76,5239,3323],{"class":86},[76,5241,1796],{"class":1549},[76,5243,3328],{"class":103},[76,5245,3331],{"class":107},[76,5247,1593],{"class":1549},[76,5249,5250,5252,5254,5256,5258,5260,5262,5264,5266,5268,5270,5272],{"class":78,"line":349},[76,5251,3338],{"class":107},[76,5253,112],{"class":111},[76,5255,2189],{"class":1560},[76,5257,1758],{"class":1549},[76,5259,3347],{"class":107},[76,5261,1354],{"class":1549},[76,5263,3352],{"class":1754},[76,5265,3355],{"class":1549},[76,5267,3358],{"class":1754},[76,5269,3361],{"class":1549},[76,5271,3364],{"class":189},[76,5273,3367],{"class":1549},[76,5275,5276,5278,5280,5282,5284,5286,5288,5290],{"class":78,"line":356},[76,5277,1681],{"class":103},[76,5279,3374],{"class":107},[76,5281,3073],{"class":111},[76,5283,3379],{"class":189},[76,5285,2321],{"class":111},[76,5287,2194],{"class":189},[76,5289,2321],{"class":111},[76,5291,3388],{"class":189},[22,5293,5295],{"id":5294},"web-interface-for-viewing-facts","Web Interface for Viewing Facts",[15,5297,5298],{},"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.",[22,5300,5302],{"id":5301},"summary","Summary",[15,5304,5305],{},"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.",[15,5307,5308],{},"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.",[15,5310,5311],{},"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.",[3409,5313],{},[15,5315,5316],{},[3414,5317,864,5318,5320],{},[73,5319,1255],{}," plugin is part of the Hermes Agent project.",[652,5322,3422],{},{"title":71,"searchDepth":118,"depth":118,"links":5324},[5325,5326,5327,5333,5334,5337,5338,5339,5340,5341,5342,5343,5350,5351],{"id":3497,"depth":118,"text":3498},{"id":3579,"depth":118,"text":3580},{"id":3592,"depth":118,"text":3593,"children":5328},[5329,5330,5331,5332],{"id":1379,"depth":349,"text":1380},{"id":1386,"depth":349,"text":1387},{"id":1393,"depth":349,"text":1394},{"id":3611,"depth":349,"text":3612},{"id":3618,"depth":118,"text":3619},{"id":3724,"depth":118,"text":3725,"children":5335},[5336],{"id":4007,"depth":349,"text":4008},{"id":4073,"depth":118,"text":4074},{"id":4223,"depth":118,"text":4224},{"id":4594,"depth":118,"text":4595},{"id":4690,"depth":118,"text":4691},{"id":4767,"depth":118,"text":4768},{"id":2855,"depth":118,"text":4833},{"id":4989,"depth":118,"text":4990,"children":5344},[5345,5346,5347,5348,5349],{"id":4993,"depth":349,"text":4994},{"id":5006,"depth":349,"text":5007},{"id":3273,"depth":349,"text":5197},{"id":5203,"depth":349,"text":5204},{"id":3289,"depth":349,"text":5212},{"id":5294,"depth":118,"text":5295},{"id":5301,"depth":118,"text":5302},"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.",{},"\u002Fblog\u002Fholographic-memory-potato-vps.en",{"title":3477,"description":5352},"blog\u002Fholographic-memory-potato-vps.en",[3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473],"Vbu0LOt9VqRHbKaDUMfhallbA3hPwesw-SEk99eUP1I",{"id":5360,"title":5361,"body":5362,"date":7071,"description":7072,"extension":673,"meta":7073,"navigation":345,"path":7074,"readingTime":676,"seo":7075,"stem":7076,"tags":7077,"__hash__":7085},"articles\u002Fblog\u002Fmulti-agent-frameworks-comparison.md","Multi-agent фреймворки: кто кого оркестрирует",{"type":8,"value":5363,"toc":7046},[5364,5368,5371,5374,5377,5381,5500,5503,5507,5510,5514,5517,5523,5529,5535,5539,5542,5549,5552,5556,5559,5562,5566,5569,5572,5575,5579,5582,5585,5589,5592,5595,5599,5602,5606,5610,5613,5616,5619,5622,5625,5824,5835,5841,5844,5847,5850,6109,6112,6115,6120,6123,6126,6129,6479,6482,6485,6488,6493,6495,6498,6501,6504,6509,6512,6515,6518,6521,6524,6529,6532,6535,6538,6542,6545,6548,6551,6554,6779,6782,6786,6984,6988,6994,7000,7006,7012,7018,7024,7028,7031,7034,7037,7040,7043],[22,5365,5367],{"id":5366},"зачем-вообще-нужны-мультиагентные-системы","Зачем вообще нужны мультиагентные системы",[15,5369,5370],{},"Представь, что ты поручил одному человеку написать статью, провести ресёрч, нарисовать иллюстрации и отредактировать текст. Он справится. Медленно, с переключением контекста, но справится. А теперь дай каждому из четырёх задачу своему специалисту — и параллельно. Результат будет быстрее и, скорее всего, качественнее.",[15,5372,5373],{},"Мультиагентные AI-системы работают по тому же принципу. Вместо одного LLM-агента, который делает всё подряд, ты запускаешь нескольких агентов с разными ролями. Один ищет информацию, другой пишет код, третий проверяет результат. Фреймворк берёт на себя оркестрацию — кто за что отвечает, как они общаются, что делать при ошибке.",[15,5375,5376],{},"В 2025–2026 эта идея вышла из стадии экспериментов в production. На GitHub десятки фреймворков, и у каждого — своя философия. Я выбрал шесть, которые реально используются, и разобрал их по косточкам.",[22,5378,5380],{"id":5379},"что-мы-сравниваем","Что мы сравниваем",[1265,5382,5383,5402],{},[1268,5384,5385],{},[1271,5386,5387,5390,5393,5396,5399],{},[1274,5388,5389],{},"Фреймворк",[1274,5391,5392],{},"Автор",[1274,5394,5395],{},"Звёзды",[1274,5397,5398],{},"Лицензия",[1274,5400,5401],{},"Последний релиз",[1287,5403,5404,5421,5438,5454,5469,5484],{},[1271,5405,5406,5409,5412,5415,5418],{},[1292,5407,5408],{},"Hermes Agent",[1292,5410,5411],{},"NousResearch",[1292,5413,5414],{},"164k",[1292,5416,5417],{},"Apache 2.0",[1292,5419,5420],{},"2026-05",[1271,5422,5423,5426,5429,5432,5435],{},[1292,5424,5425],{},"CrewAI",[1292,5427,5428],{},"crewAI Inc",[1292,5430,5431],{},"52k",[1292,5433,5434],{},"MIT",[1292,5436,5437],{},"2026-05-18",[1271,5439,5440,5443,5446,5449,5451],{},[1292,5441,5442],{},"LangGraph",[1292,5444,5445],{},"LangChain",[1292,5447,5448],{},"33k",[1292,5450,5434],{},[1292,5452,5453],{},"2026-05-22",[1271,5455,5456,5459,5462,5465,5467],{},[1292,5457,5458],{},"CAMEL-AI",[1292,5460,5461],{},"camel-ai",[1292,5463,5464],{},"17k",[1292,5466,5417],{},[1292,5468,5420],{},[1271,5470,5471,5474,5477,5480,5482],{},[1292,5472,5473],{},"AG2",[1292,5475,5476],{},"ag2ai (ex-AutoGen)",[1292,5478,5479],{},"4.6k",[1292,5481,5417],{},[1292,5483,5420],{},[1271,5485,5486,5489,5492,5495,5497],{},[1292,5487,5488],{},"OpenPlanter",[1292,5490,5491],{},"ShinMegamiBoson",[1292,5493,5494],{},"1.6k",[1292,5496,5434],{},[1292,5498,5499],{},"2026-03",[15,5501,5502],{},"Все — на Python. Все open source. Но на этом сходства заканчиваются.",[22,5504,5506],{"id":5505},"какие-фичи-важны-и-что-они-значат","Какие фичи важны (и что они значат)",[15,5508,5509],{},"Прежде чем лезть в сравнение, разберёмся с терминологией. В таблицах и доках фреймворков часто фигурируют одни и те же слова, которые значат совершенно разные вещи.",[137,5511,5513],{"id":5512},"оркестрация-single-multi-parallel","Оркестрация (single \u002F multi \u002F parallel)",[15,5515,5516],{},"Это сердце любого мультиагентного фреймворка — как агенты координируют работу.",[15,5518,5519,5522],{},[478,5520,5521],{},"Single"," — один агент, одна задача. По сути, обычный LLM-вызов. OpenPlanter работает именно так: ты запускаешь цепочку, но внутри неё нет реально параллельных агентов.",[15,5524,5525,5528],{},[478,5526,5527],{},"Multi"," — несколько агентов, но работают они по очереди или по схеме «ведущий ведомый». AG2, CrewAI и CAMEL-AI используют этот подход. Один агент может делегировать задачу другому, но они не тянут одновременно.",[15,5530,5531,5534],{},[478,5532,5533],{},"Parallel"," — агенты реально работают параллельно, с чекпоинтами и условной маршрутизацией. LangGraph тут впереди: его DAG (Directed Acyclic Graph) позволяет ветвить и мержить потоки выполнения. Hermes Agent тоже поддерживает multi через Kanban-доску с зависимостями между задачами.",[137,5536,5538],{"id":5537},"разделяемая-память","Разделяемая память",[15,5540,5541],{},"Могут ли агенты видеть контекст друг друга? Это критично для сложных пайплайнов, где результат одного агента — входные данные для другого.",[15,5543,5544,5545,5548],{},"AG2 реализует это через ",[73,5546,5547],{},"context_variables"," — общий словарь, который читают и пишут все агенты в Group Chat. CrewAI и LangGraph тоже поддерживают shared state. CAMEL-AI имеет общую память через паттерн Workforce.",[15,5550,5551],{},"У Hermes Agent shared memory частичная — есть issue #377 с предложением scratchpad-паттерна, но оно пока открыто. Kanban-задачи обеспечивают обмен данными через метаданные и комментарии, но это не то же самое, что общий пул памяти в реальном времени.",[137,5553,5555],{"id":5554},"коммуникация","Коммуникация",[15,5557,5558],{},"Как агенты разговаривают друг с другом? Все шесть фреймворков используют internal-коммуникацию — сообщения передаются внутри фреймворка, не через внешний API. Это нормально для большинства кейсов, но становится проблемой, когда нужно связать агентов из разных систем.",[15,5560,5561],{},"Тут на помощь приходит A2A-протокол (об этом ниже).",[137,5563,5565],{"id":5564},"adversarial-debate-антагонистический-дебат","Adversarial debate (антагонистический дебат)",[15,5567,5568],{},"Один агент генерирует ответ, другой его критикует. Если критик находит проблемы — первый переделывает. Это мощный паттерн для повышения качества: по сути, code review для LLM-вывода.",[15,5570,5571],{},"AG2 реализует это через Group Chat с LLM-handoffs. CAMEL-AI идёт дальше — у него RolePlaying-паттерн изначально спроектирован для дебатов. Hermes Agent имеет partial-поддержку через PR #20158 (режим Adversarial Debate Mode).",[15,5573,5574],{},"CrewAI, LangGraph и OpenPlanter — без встроенного дебата. Можно эмулировать через дополнительные агенты с промптами, но из коробки не работает.",[137,5576,5578],{"id":5577},"inception-prompting","Inception prompting",[15,5580,5581],{},"Самоулучшающиеся промпты — агент анализирует свой вывод и корректирует системный промпт для следующей попытки. Звучит как sci-fi, но CAMEL-AI реализует это через Inception Prompting из своей исследовательской работы (NeurIPS 2023).",[15,5583,5584],{},"У AG2 partial-поддержка через системные сообщения. У остальных — нет.",[137,5586,5588],{"id":5587},"adaptive-retry","Adaptive retry",[15,5590,5591],{},"Что происходит, когда агент ошибается? Простой вариант — повторить тот же запрос. Умный вариант — проанализировать ошибку, сменить модель, скорректировать промпт, и только потом повторить.",[15,5593,5594],{},"Hermes Agent планирует Adaptive Retry с лестницей эскалации модели (issue #30587, PR #30620). CrewAI и LangGraph имеют retry-механизмы. AG2, CAMEL-AI и OpenPlanter — нет.",[137,5596,5598],{"id":5597},"managed-runtime","Managed runtime",[15,5600,5601],{},"Нужно ли тебе управлять инфраструктурой, на которой работают агенты? CrewAI и LangGraph предлагают managed-решения — облако, где агенты уже запущены и настроены. AG2 и Hermes Agent — partial: можно запустить самому, но есть некоторые контракты для управления средой выполнения. CAMEL-AI и OpenPlanter — полностью self-hosted.",[22,5603,5605],{"id":5604},"фреймворк-за-фреймворком","Фреймворк за фреймворком",[137,5607,5609],{"id":5608},"ag2-ex-autogen","AG2 (ex-AutoGen)",[15,5611,5612],{},"AG2 — это переименованный AutoGen от Microsoft, который теперь живёт в организации ag2ai. Самый зрелый фреймворк в плане оркестрации.",[15,5614,5615],{},"У AG2 пять паттернов координации: AutoPattern (LLM выбирает следующего спикера), RoundRobin (по кругу), Random (случайно), Manual (человек выбирает), Default (явные handoffs с условиями). Это гибко — можно выстроить практически любую топологию.",[15,5617,5618],{},"Главный козырь AG2 — нативная поддержка A2A с версии 0.10. Ты exposing своего агента как A2A-сервер, и к нему могут подключаться агенты из других фреймворков. На сегодня AG2 единственный крупный фреймворк, у которого A2A работает «из коробки» и совместим с v1.0 спецификации.",[15,5620,5621],{},"Слабые стороны: нет quality gates (агент не проверяет качество вывода подчинённых), нет персистентных профилей, нет CLI-интерфейса — только Python-библиотека. Если тебе нужен «агент как сервис» с CLI и плагинами, AG2 не про это.",[15,5623,5624],{},"Вот так выглядит оркестрация в AG2:",[66,5626,5628],{"className":1527,"code":5627,"language":1529,"meta":71,"style":71},"from autogen import ConversableAgent, GroupChat, GroupChatManager\n\ncoder = ConversableAgent(\"coder\", system_message=\"Ты пишешь Python-код.\")\nreviewer = ConversableAgent(\"reviewer\", system_message=\"Ты проверяешь код и находишь баги.\")\nwriter = ConversableAgent(\"writer\", system_message=\"Ты пишешь документацию.\")\n\ngroupchat = GroupChat(\n    agents=[coder, reviewer, writer],\n    messages=[],\n    speaker_selection_method=\"auto\",  # LLM выбирает, кто говорит следующим\n)\nmanager = GroupChatManager(groupchat=groupchat)\n",[73,5629,5630,5653,5657,5683,5708,5733,5737,5749,5774,5784,5799,5803],{"__ignoreMap":71},[76,5631,5632,5635,5638,5640,5643,5645,5648,5650],{"class":78,"line":79},[76,5633,5634],{"class":103},"from",[76,5636,5637],{"class":107}," autogen ",[76,5639,1740],{"class":103},[76,5641,5642],{"class":107}," ConversableAgent",[76,5644,1919],{"class":1549},[76,5646,5647],{"class":107}," GroupChat",[76,5649,1919],{"class":1549},[76,5651,5652],{"class":107}," GroupChatManager\n",[76,5654,5655],{"class":78,"line":118},[76,5656,346],{"emptyLinePlaceholder":345},[76,5658,5659,5662,5664,5666,5668,5671,5673,5676,5678,5681],{"class":78,"line":349},[76,5660,5661],{"class":107},"coder ",[76,5663,112],{"class":111},[76,5665,5642],{"class":1754},[76,5667,1758],{"class":1549},[76,5669,5670],{"class":86},"\"coder\"",[76,5672,1919],{"class":1549},[76,5674,5675],{"class":2267}," system_message",[76,5677,112],{"class":111},[76,5679,5680],{"class":86},"\"Ты пишешь Python-код.\"",[76,5682,1764],{"class":1549},[76,5684,5685,5688,5690,5692,5694,5697,5699,5701,5703,5706],{"class":78,"line":356},[76,5686,5687],{"class":107},"reviewer ",[76,5689,112],{"class":111},[76,5691,5642],{"class":1754},[76,5693,1758],{"class":1549},[76,5695,5696],{"class":86},"\"reviewer\"",[76,5698,1919],{"class":1549},[76,5700,5675],{"class":2267},[76,5702,112],{"class":111},[76,5704,5705],{"class":86},"\"Ты проверяешь код и находишь баги.\"",[76,5707,1764],{"class":1549},[76,5709,5710,5713,5715,5717,5719,5722,5724,5726,5728,5731],{"class":78,"line":365},[76,5711,5712],{"class":107},"writer ",[76,5714,112],{"class":111},[76,5716,5642],{"class":1754},[76,5718,1758],{"class":1549},[76,5720,5721],{"class":86},"\"writer\"",[76,5723,1919],{"class":1549},[76,5725,5675],{"class":2267},[76,5727,112],{"class":111},[76,5729,5730],{"class":86},"\"Ты пишешь документацию.\"",[76,5732,1764],{"class":1549},[76,5734,5735],{"class":78,"line":370},[76,5736,346],{"emptyLinePlaceholder":345},[76,5738,5739,5742,5744,5746],{"class":78,"line":376},[76,5740,5741],{"class":107},"groupchat ",[76,5743,112],{"class":111},[76,5745,5647],{"class":1754},[76,5747,5748],{"class":1549},"(\n",[76,5750,5751,5754,5756,5758,5761,5763,5766,5768,5771],{"class":78,"line":1628},[76,5752,5753],{"class":2267},"    agents",[76,5755,112],{"class":111},[76,5757,1557],{"class":1549},[76,5759,5760],{"class":107},"coder",[76,5762,1919],{"class":1549},[76,5764,5765],{"class":107}," reviewer",[76,5767,1919],{"class":1549},[76,5769,5770],{"class":107}," writer",[76,5772,5773],{"class":1549},"],\n",[76,5775,5776,5779,5781],{"class":78,"line":1636},[76,5777,5778],{"class":2267},"    messages",[76,5780,112],{"class":111},[76,5782,5783],{"class":1549},"[],\n",[76,5785,5786,5789,5791,5794,5796],{"class":78,"line":1645},[76,5787,5788],{"class":2267},"    speaker_selection_method",[76,5790,112],{"class":111},[76,5792,5793],{"class":86},"\"auto\"",[76,5795,1919],{"class":1549},[76,5797,5798],{"class":352},"  # LLM выбирает, кто говорит следующим\n",[76,5800,5801],{"class":78,"line":1656},[76,5802,1764],{"class":1549},[76,5804,5805,5808,5810,5813,5815,5818,5820,5822],{"class":78,"line":1668},[76,5806,5807],{"class":107},"manager ",[76,5809,112],{"class":111},[76,5811,5812],{"class":1754}," GroupChatManager",[76,5814,1758],{"class":1549},[76,5816,5817],{"class":2267},"groupchat",[76,5819,112],{"class":111},[76,5821,5817],{"class":107},[76,5823,1764],{"class":1549},[15,5825,5826,5827,5830,5831,5834],{},"DefaultPattern даёт ещё больше контроля — явные условия перехода между агентами с ",[73,5828,5829],{},"OnCondition"," и ",[73,5832,5833],{},"LLMCondition",". Это как state machine, только вместо состояний — агенты.",[15,5836,5837,5840],{},[478,5838,5839],{},"Когда выбирать:"," нужна гибкая оркестрация с A2A-интеропом между разными фреймворками.",[137,5842,5425],{"id":5843},"crewai",[15,5845,5846],{},"CrewAI — самый популярный фреймворк после Hermes. 52k звёзд, MIT-лицения, простой API. Философия — «ролевые команды»: ты определяешь агентов с ролями (researcher, writer, analyst), даёшь им задачи, и CrewAI оркестрирует выполнение.",[15,5848,5849],{},"CrewAI выглядит примерно так:",[66,5851,5853],{"className":1527,"code":5852,"language":1529,"meta":71,"style":71},"from crewai import Agent, Task, Crew\n\nresearcher = Agent(\n    role=\"Research Analyst\",\n    goal=\"Find relevant data on the topic\",\n    backstory=\"You are an expert researcher with 10 years of experience.\",\n)\nwriter = Agent(\n    role=\"Content Writer\",\n    goal=\"Write a clear, engaging article\",\n    backstory=\"You are a skilled technical writer.\",\n)\n\nresearch_task = Task(description=\"Research the topic\", agent=researcher)\nwrite_task = Task(description=\"Write the article\", agent=writer)\n\ncrew = Crew(agents=[researcher, writer], tasks=[research_task, write_task])\nresult = crew.kickoff()\n",[73,5854,5855,5877,5881,5892,5904,5916,5928,5932,5942,5953,5964,5975,5979,5983,6014,6043,6047,6092],{"__ignoreMap":71},[76,5856,5857,5859,5862,5864,5867,5869,5872,5874],{"class":78,"line":79},[76,5858,5634],{"class":103},[76,5860,5861],{"class":107}," crewai ",[76,5863,1740],{"class":103},[76,5865,5866],{"class":107}," Agent",[76,5868,1919],{"class":1549},[76,5870,5871],{"class":107}," Task",[76,5873,1919],{"class":1549},[76,5875,5876],{"class":107}," Crew\n",[76,5878,5879],{"class":78,"line":118},[76,5880,346],{"emptyLinePlaceholder":345},[76,5882,5883,5886,5888,5890],{"class":78,"line":349},[76,5884,5885],{"class":107},"researcher ",[76,5887,112],{"class":111},[76,5889,5866],{"class":1754},[76,5891,5748],{"class":1549},[76,5893,5894,5897,5899,5902],{"class":78,"line":356},[76,5895,5896],{"class":2267},"    role",[76,5898,112],{"class":111},[76,5900,5901],{"class":86},"\"Research Analyst\"",[76,5903,2010],{"class":1549},[76,5905,5906,5909,5911,5914],{"class":78,"line":365},[76,5907,5908],{"class":2267},"    goal",[76,5910,112],{"class":111},[76,5912,5913],{"class":86},"\"Find relevant data on the topic\"",[76,5915,2010],{"class":1549},[76,5917,5918,5921,5923,5926],{"class":78,"line":370},[76,5919,5920],{"class":2267},"    backstory",[76,5922,112],{"class":111},[76,5924,5925],{"class":86},"\"You are an expert researcher with 10 years of experience.\"",[76,5927,2010],{"class":1549},[76,5929,5930],{"class":78,"line":376},[76,5931,1764],{"class":1549},[76,5933,5934,5936,5938,5940],{"class":78,"line":1628},[76,5935,5712],{"class":107},[76,5937,112],{"class":111},[76,5939,5866],{"class":1754},[76,5941,5748],{"class":1549},[76,5943,5944,5946,5948,5951],{"class":78,"line":1636},[76,5945,5896],{"class":2267},[76,5947,112],{"class":111},[76,5949,5950],{"class":86},"\"Content Writer\"",[76,5952,2010],{"class":1549},[76,5954,5955,5957,5959,5962],{"class":78,"line":1645},[76,5956,5908],{"class":2267},[76,5958,112],{"class":111},[76,5960,5961],{"class":86},"\"Write a clear, engaging article\"",[76,5963,2010],{"class":1549},[76,5965,5966,5968,5970,5973],{"class":78,"line":1656},[76,5967,5920],{"class":2267},[76,5969,112],{"class":111},[76,5971,5972],{"class":86},"\"You are a skilled technical writer.\"",[76,5974,2010],{"class":1549},[76,5976,5977],{"class":78,"line":1668},[76,5978,1764],{"class":1549},[76,5980,5981],{"class":78,"line":1678},[76,5982,346],{"emptyLinePlaceholder":345},[76,5984,5985,5988,5990,5992,5994,5997,5999,6002,6004,6007,6009,6012],{"class":78,"line":1686},[76,5986,5987],{"class":107},"research_task ",[76,5989,112],{"class":111},[76,5991,5871],{"class":1754},[76,5993,1758],{"class":1549},[76,5995,5996],{"class":2267},"description",[76,5998,112],{"class":111},[76,6000,6001],{"class":86},"\"Research the topic\"",[76,6003,1919],{"class":1549},[76,6005,6006],{"class":2267}," agent",[76,6008,112],{"class":111},[76,6010,6011],{"class":107},"researcher",[76,6013,1764],{"class":1549},[76,6015,6016,6019,6021,6023,6025,6027,6029,6032,6034,6036,6038,6041],{"class":78,"line":1691},[76,6017,6018],{"class":107},"write_task ",[76,6020,112],{"class":111},[76,6022,5871],{"class":1754},[76,6024,1758],{"class":1549},[76,6026,5996],{"class":2267},[76,6028,112],{"class":111},[76,6030,6031],{"class":86},"\"Write the article\"",[76,6033,1919],{"class":1549},[76,6035,6006],{"class":2267},[76,6037,112],{"class":111},[76,6039,6040],{"class":107},"writer",[76,6042,1764],{"class":1549},[76,6044,6045],{"class":78,"line":1702},[76,6046,346],{"emptyLinePlaceholder":345},[76,6048,6049,6052,6054,6057,6059,6062,6064,6066,6068,6070,6072,6075,6078,6080,6082,6085,6087,6090],{"class":78,"line":1708},[76,6050,6051],{"class":107},"crew ",[76,6053,112],{"class":111},[76,6055,6056],{"class":1754}," Crew",[76,6058,1758],{"class":1549},[76,6060,6061],{"class":2267},"agents",[76,6063,112],{"class":111},[76,6065,1557],{"class":1549},[76,6067,6011],{"class":107},[76,6069,1919],{"class":1549},[76,6071,5770],{"class":107},[76,6073,6074],{"class":1549},"],",[76,6076,6077],{"class":2267}," tasks",[76,6079,112],{"class":111},[76,6081,1557],{"class":1549},[76,6083,6084],{"class":107},"research_task",[76,6086,1919],{"class":1549},[76,6088,6089],{"class":107}," write_task",[76,6091,3367],{"class":1549},[76,6093,6094,6097,6099,6102,6104,6107],{"class":78,"line":1717},[76,6095,6096],{"class":107},"result ",[76,6098,112],{"class":111},[76,6100,6101],{"class":107}," crew",[76,6103,1354],{"class":1549},[76,6105,6106],{"class":1754},"kickoff",[76,6108,1821],{"class":1549},[15,6110,6111],{},"Два режима: sequential (задачи по очереди) и hierarchical (менеджер делегирует работникам). Есть managed runtime — можно запустить в облаке CrewAI. Adaptive retry — да, встроен.",[15,6113,6114],{},"Слабые стороны: нет adversarial debate, слабая поддержка сложных DAG (это не LangGraph), state persistence ограничен. A2A — только community-адаптеры, нативной поддержки нет.",[15,6116,6117,6119],{},[478,6118,5839],{}," нужен быстрый старт с понятной ролевой моделью. Типичный кейс: «researcher ищет информацию, writer пишет отчёт, reviewer проверяет».",[137,6121,5442],{"id":6122},"langgraph",[15,6124,6125],{},"LangGraph — это надстройка над LangChain, которая превращает цепочки вызовов в граф. Каждый узел — шаг обработки, рёбра — условия перехода. Можно ветвить, мержить, делать checkpoint, вставлять human-in-the-loop.",[15,6127,6128],{},"Вот схематично, как выглядит граф:",[66,6130,6132],{"className":1527,"code":6131,"language":1529,"meta":71,"style":71},"from langgraph.graph import StateGraph\n\ndef research(state):\n    return {\"data\": call_llm(\"Research: \" + state[\"topic\"])}\n\ndef write(state):\n    return {\"draft\": call_llm(\"Write about: \" + state[\"data\"])}\n\ndef review(state):\n    approved = call_llm(\"Is this good? \" + state[\"draft\"])\n    return {\"approved\": \"yes\" in approved.lower()}\n\ngraph = StateGraph(dict)\ngraph.add_node(\"research\", research)\ngraph.add_node(\"write\", write)\ngraph.add_node(\"review\", review)\ngraph.add_edge(\"research\", \"write\")\ngraph.add_conditional_edges(\"review\", lambda s: \"end\" if s[\"approved\"] else \"write\")\n",[73,6133,6134,6151,6155,6169,6207,6211,6224,6257,6261,6274,6303,6331,6335,6351,6372,6391,6410,6430],{"__ignoreMap":71},[76,6135,6136,6138,6141,6143,6146,6148],{"class":78,"line":79},[76,6137,5634],{"class":103},[76,6139,6140],{"class":107}," langgraph",[76,6142,1354],{"class":1549},[76,6144,6145],{"class":107},"graph ",[76,6147,1740],{"class":103},[76,6149,6150],{"class":107}," StateGraph\n",[76,6152,6153],{"class":78,"line":118},[76,6154,346],{"emptyLinePlaceholder":345},[76,6156,6157,6159,6162,6164,6167],{"class":78,"line":349},[76,6158,1578],{"class":103},[76,6160,6161],{"class":82}," research",[76,6163,1758],{"class":1549},[76,6165,6166],{"class":1553},"state",[76,6168,2404],{"class":1549},[76,6170,6171,6173,6176,6179,6181,6184,6186,6189,6191,6194,6196,6198,6202,6204],{"class":78,"line":356},[76,6172,1681],{"class":103},[76,6174,6175],{"class":1549}," {",[76,6177,6178],{"class":86},"\"data\"",[76,6180,1550],{"class":1549},[76,6182,6183],{"class":1754}," call_llm",[76,6185,1758],{"class":1549},[76,6187,6188],{"class":86},"\"Research: \"",[76,6190,2360],{"class":111},[76,6192,6193],{"class":1553}," state",[76,6195,1557],{"class":1549},[76,6197,441],{"class":86},[76,6199,6201],{"class":6200},"sdETa","topic",[76,6203,441],{"class":86},[76,6205,6206],{"class":1549},"])}\n",[76,6208,6209],{"class":78,"line":365},[76,6210,346],{"emptyLinePlaceholder":345},[76,6212,6213,6215,6218,6220,6222],{"class":78,"line":370},[76,6214,1578],{"class":103},[76,6216,6217],{"class":82}," write",[76,6219,1758],{"class":1549},[76,6221,6166],{"class":1553},[76,6223,2404],{"class":1549},[76,6225,6226,6228,6230,6233,6235,6237,6239,6242,6244,6246,6248,6250,6253,6255],{"class":78,"line":376},[76,6227,1681],{"class":103},[76,6229,6175],{"class":1549},[76,6231,6232],{"class":86},"\"draft\"",[76,6234,1550],{"class":1549},[76,6236,6183],{"class":1754},[76,6238,1758],{"class":1549},[76,6240,6241],{"class":86},"\"Write about: \"",[76,6243,2360],{"class":111},[76,6245,6193],{"class":1553},[76,6247,1557],{"class":1549},[76,6249,441],{"class":86},[76,6251,6252],{"class":6200},"data",[76,6254,441],{"class":86},[76,6256,6206],{"class":1549},[76,6258,6259],{"class":78,"line":1628},[76,6260,346],{"emptyLinePlaceholder":345},[76,6262,6263,6265,6268,6270,6272],{"class":78,"line":1636},[76,6264,1578],{"class":103},[76,6266,6267],{"class":82}," review",[76,6269,1758],{"class":1549},[76,6271,6166],{"class":1553},[76,6273,2404],{"class":1549},[76,6275,6276,6279,6281,6283,6285,6288,6290,6292,6294,6296,6299,6301],{"class":78,"line":1645},[76,6277,6278],{"class":107},"    approved ",[76,6280,112],{"class":111},[76,6282,6183],{"class":1754},[76,6284,1758],{"class":1549},[76,6286,6287],{"class":86},"\"Is this good? \"",[76,6289,2360],{"class":111},[76,6291,6193],{"class":1553},[76,6293,1557],{"class":1549},[76,6295,441],{"class":86},[76,6297,6298],{"class":6200},"draft",[76,6300,441],{"class":86},[76,6302,3367],{"class":1549},[76,6304,6305,6307,6309,6312,6314,6317,6320,6323,6325,6328],{"class":78,"line":1656},[76,6306,1681],{"class":103},[76,6308,6175],{"class":1549},[76,6310,6311],{"class":86},"\"approved\"",[76,6313,1550],{"class":1549},[76,6315,6316],{"class":86}," \"yes\"",[76,6318,6319],{"class":103}," in",[76,6321,6322],{"class":107}," approved",[76,6324,1354],{"class":1549},[76,6326,6327],{"class":1754},"lower",[76,6329,6330],{"class":1549},"()}\n",[76,6332,6333],{"class":78,"line":1668},[76,6334,346],{"emptyLinePlaceholder":345},[76,6336,6337,6339,6341,6344,6346,6349],{"class":78,"line":1678},[76,6338,6145],{"class":107},[76,6340,112],{"class":111},[76,6342,6343],{"class":1754}," StateGraph",[76,6345,1758],{"class":1549},[76,6347,6348],{"class":1560},"dict",[76,6350,1764],{"class":1549},[76,6352,6353,6356,6358,6361,6363,6366,6368,6370],{"class":78,"line":1686},[76,6354,6355],{"class":107},"graph",[76,6357,1354],{"class":1549},[76,6359,6360],{"class":1754},"add_node",[76,6362,1758],{"class":1549},[76,6364,6365],{"class":86},"\"research\"",[76,6367,1919],{"class":1549},[76,6369,6161],{"class":107},[76,6371,1764],{"class":1549},[76,6373,6374,6376,6378,6380,6382,6385,6387,6389],{"class":78,"line":1691},[76,6375,6355],{"class":107},[76,6377,1354],{"class":1549},[76,6379,6360],{"class":1754},[76,6381,1758],{"class":1549},[76,6383,6384],{"class":86},"\"write\"",[76,6386,1919],{"class":1549},[76,6388,6217],{"class":107},[76,6390,1764],{"class":1549},[76,6392,6393,6395,6397,6399,6401,6404,6406,6408],{"class":78,"line":1702},[76,6394,6355],{"class":107},[76,6396,1354],{"class":1549},[76,6398,6360],{"class":1754},[76,6400,1758],{"class":1549},[76,6402,6403],{"class":86},"\"review\"",[76,6405,1919],{"class":1549},[76,6407,6267],{"class":107},[76,6409,1764],{"class":1549},[76,6411,6412,6414,6416,6419,6421,6423,6425,6428],{"class":78,"line":1708},[76,6413,6355],{"class":107},[76,6415,1354],{"class":1549},[76,6417,6418],{"class":1754},"add_edge",[76,6420,1758],{"class":1549},[76,6422,6365],{"class":86},[76,6424,1919],{"class":1549},[76,6426,6427],{"class":86}," \"write\"",[76,6429,1764],{"class":1549},[76,6431,6432,6434,6436,6439,6441,6443,6445,6448,6451,6453,6456,6459,6461,6463,6465,6468,6470,6472,6475,6477],{"class":78,"line":1717},[76,6433,6355],{"class":107},[76,6435,1354],{"class":1549},[76,6437,6438],{"class":1754},"add_conditional_edges",[76,6440,1758],{"class":1549},[76,6442,6403],{"class":86},[76,6444,1919],{"class":1549},[76,6446,6447],{"class":103}," lambda",[76,6449,6450],{"class":1553}," s",[76,6452,1550],{"class":1549},[76,6454,6455],{"class":86}," \"end\"",[76,6457,6458],{"class":103}," if",[76,6460,6450],{"class":1553},[76,6462,1557],{"class":1549},[76,6464,441],{"class":86},[76,6466,6467],{"class":6200},"approved",[76,6469,441],{"class":86},[76,6471,1564],{"class":1549},[76,6473,6474],{"class":103}," else",[76,6476,6427],{"class":86},[76,6478,1764],{"class":1549},[15,6480,6481],{},"Сильная сторона — stateful workflows с чекпоинтами. Если агент упал на шаге 7 из 12, можно откатиться к шагу 6 и продолжить. Это редкость среди мультиагентных фреймворков. Human-in-the-loop тоже хорошо реализован — можно вставить «паузу» в граф, чтобы человек подтвердил переход.",[15,6483,6484],{},"33k звёзд, MIT, managed runtime через LangGraph Cloud. Adaptive retry есть.",[15,6486,6487],{},"Слабые стороны: нет встроенного adversarial debate, нет inception prompting, A2A только через community. LangGraph больше про workflow-оркестрацию, чем про «агенты, которые спорят друг с другом».",[15,6489,6490,6492],{},[478,6491,5839],{}," сложные многошаговые пайплайны с ветвлениями, чекпоинтами и human-in-the-loop. Если тебе нужен «граф обработки с возможностью отката» — это LangGraph.",[137,6494,5458],{"id":5461},[15,6496,6497],{},"CAMEL-AI — самый исследовательский фреймворк. Родился как академический проект (NeurIPS 2023, arXiv:2303.17760) и до сих пор сохраняет research-DNA.",[15,6499,6500],{},"У CAMEL-AI два уникальных паттерна: RolePlaying (два агента играют роли и спорят, пока не придут к консенсусу) и Inception Prompting (агент улучшает свои промпты на основе предыдущих попыток). Workforce-паттерн реализует shared memory для команд агентов.",[15,6502,6503],{},"17k звёзд, Apache 2.0. Из минусов — нет managed runtime, нет adaptive retry, нет A2A-поддержки. Фреймворк больше заточен под эксперименты, чем под production.",[15,6505,6506,6508],{},[478,6507,5839],{}," исследовательские задачи, где нужен adversarial debate и inception prompting. Если ты изучаешь, как LLM-агенты могут спорить и самообучаться — CAMEL-AI это место.",[137,6510,5408],{"id":6511},"hermes-agent",[15,6513,6514],{},"Hermes Agent от NousResearch — не совсем фреймворк в классическом смысле. Это CLI-first система с Kanban-доской, профилями, навыками и плагинами. 164k звёзд — самый популярный в списке.",[15,6516,6517],{},"Философия другая: Hermes решает application layer (задачи, память, навыки, CLI), а не framework layer (Python SDK для написания агентов). Kanban-доска с зависимостями — это и есть оркестрация: задачи блокируются, разблокируются, передаются между профилями агентов.",[15,6519,6520],{},"Shared memory partial, adversarial debate partial (PR #20158), adaptive retry в разработке (PR #30620). A2A — реализация существует (PR #4135, +2831 строк, 71 тест), но не смержена.",[15,6522,6523],{},"Главная сила Hermes — экосистема. Persistent profiles (изолированная конфигурация, память, навыки для каждого агента), Skills system (процедурная память), Holographic Memory (гибридный поиск с FTS5 + семантика). Никакой другой фреймворк не даёт ничего подобного.",[15,6525,6526,6528],{},[478,6527,5839],{}," нужна CLI-система с persistent агентами, задачами и навыками. Hermes — это не «библиотека для написания агентов», а «платформа для запуска агентов как сервисов».",[137,6530,5488],{"id":6531},"openplanter",[15,6533,6534],{},"OpenPlanter — самый маленький фреймворк в подборке. 1.6k звёзд, single orchestration, без shared memory, без debate, без retry. По сути, это обёртка для последовательного вызова LLM с минимальной оркестрацией.",[15,6536,6537],{},"Не буду рекомендовать его для production. Но если тебе нужен простой каркас для экспериментов — OpenPlanter может быть отправной точкой. Иногда «less is more» работает.",[22,6539,6541],{"id":6540},"a2a-протокол-почему-это-важно","A2A-протокол: почему это важно",[15,6543,6544],{},"Google представил A2A (Agent-to-Agent) протокол в апреле 2025 года. Идея простая: MCP отвечает на вопрос «какие инструменты доступны?», а A2A — «кто может помочь?»",[15,6546,6547],{},"Ключевые концепции: Agent Card (JSON-описание возможностей агента, аналог MCP tool list), Task (единица работы), Message (коммуникация внутри задачи), Artifact (результат). Есть streaming через SSE.",[15,6549,6550],{},"Сейчас A2A поддерживают нативно: AG2 (с v0.10), Google ADK (с первого дня), Pydantic AI. Community-адаптеры есть у CrewAI и LangChain. Hermes Agent — полная реализация существует, но PR #4135 не смержен.",[15,6552,6553],{},"Почему это важно? Потому что мультиагентные фреймворки — это острова. Твой агент на CrewAI не может легко позвать агента на AG2. A2A решает эту проблему: expose агента как A2A-сервер, и любой фреймворк с A2A-клиентом может к нему обратиться.",[66,6555,6557],{"className":1527,"code":6556,"language":1529,"meta":71,"style":71},"# AG2: exposing агента как A2A-сервер\nfrom autogen import ConversableAgent, LLMConfig\nfrom autogen.a2a import A2aAgentServer\n\nagent = ConversableAgent(\n    name=\"coder\",\n    system_message=\"Expert Python developer\",\n    llm_config=LLMConfig({\"model\": \"gpt-4o-mini\"}),\n)\nserver = A2aAgentServer(agent).build()\n# uvicorn server:server --port 8000\n\n# Подключение из другого процесса\nfrom autogen.a2a import A2aRemoteAgent\nremote = A2aRemoteAgent(url=\"http:\u002F\u002Flocalhost:8000\", name=\"coder\")\nawait local_agent.a_initiate_chat(recipient=remote, message=\"Write a CSV parser\")\n",[73,6558,6559,6564,6579,6596,6600,6611,6622,6634,6658,6662,6684,6689,6693,6698,6713,6744],{"__ignoreMap":71},[76,6560,6561],{"class":78,"line":79},[76,6562,6563],{"class":352},"# AG2: exposing агента как A2A-сервер\n",[76,6565,6566,6568,6570,6572,6574,6576],{"class":78,"line":118},[76,6567,5634],{"class":103},[76,6569,5637],{"class":107},[76,6571,1740],{"class":103},[76,6573,5642],{"class":107},[76,6575,1919],{"class":1549},[76,6577,6578],{"class":107}," LLMConfig\n",[76,6580,6581,6583,6586,6588,6591,6593],{"class":78,"line":349},[76,6582,5634],{"class":103},[76,6584,6585],{"class":107}," autogen",[76,6587,1354],{"class":1549},[76,6589,6590],{"class":107},"a2a ",[76,6592,1740],{"class":103},[76,6594,6595],{"class":107}," A2aAgentServer\n",[76,6597,6598],{"class":78,"line":356},[76,6599,346],{"emptyLinePlaceholder":345},[76,6601,6602,6605,6607,6609],{"class":78,"line":365},[76,6603,6604],{"class":107},"agent ",[76,6606,112],{"class":111},[76,6608,5642],{"class":1754},[76,6610,5748],{"class":1549},[76,6612,6613,6616,6618,6620],{"class":78,"line":370},[76,6614,6615],{"class":2267},"    name",[76,6617,112],{"class":111},[76,6619,5670],{"class":86},[76,6621,2010],{"class":1549},[76,6623,6624,6627,6629,6632],{"class":78,"line":376},[76,6625,6626],{"class":2267},"    system_message",[76,6628,112],{"class":111},[76,6630,6631],{"class":86},"\"Expert Python developer\"",[76,6633,2010],{"class":1549},[76,6635,6636,6639,6641,6644,6647,6650,6652,6655],{"class":78,"line":1628},[76,6637,6638],{"class":2267},"    llm_config",[76,6640,112],{"class":111},[76,6642,6643],{"class":1754},"LLMConfig",[76,6645,6646],{"class":1549},"({",[76,6648,6649],{"class":86},"\"model\"",[76,6651,1550],{"class":1549},[76,6653,6654],{"class":86}," \"gpt-4o-mini\"",[76,6656,6657],{"class":1549},"}),\n",[76,6659,6660],{"class":78,"line":1636},[76,6661,1764],{"class":1549},[76,6663,6664,6667,6669,6672,6674,6677,6679,6682],{"class":78,"line":1645},[76,6665,6666],{"class":107},"server ",[76,6668,112],{"class":111},[76,6670,6671],{"class":1754}," A2aAgentServer",[76,6673,1758],{"class":1549},[76,6675,6676],{"class":107},"agent",[76,6678,2281],{"class":1549},[76,6680,6681],{"class":1754},"build",[76,6683,1821],{"class":1549},[76,6685,6686],{"class":78,"line":1656},[76,6687,6688],{"class":352},"# uvicorn server:server --port 8000\n",[76,6690,6691],{"class":78,"line":1668},[76,6692,346],{"emptyLinePlaceholder":345},[76,6694,6695],{"class":78,"line":1678},[76,6696,6697],{"class":352},"# Подключение из другого процесса\n",[76,6699,6700,6702,6704,6706,6708,6710],{"class":78,"line":1686},[76,6701,5634],{"class":103},[76,6703,6585],{"class":107},[76,6705,1354],{"class":1549},[76,6707,6590],{"class":107},[76,6709,1740],{"class":103},[76,6711,6712],{"class":107}," A2aRemoteAgent\n",[76,6714,6715,6718,6720,6723,6725,6728,6730,6733,6735,6738,6740,6742],{"class":78,"line":1691},[76,6716,6717],{"class":107},"remote ",[76,6719,112],{"class":111},[76,6721,6722],{"class":1754}," A2aRemoteAgent",[76,6724,1758],{"class":1549},[76,6726,6727],{"class":2267},"url",[76,6729,112],{"class":111},[76,6731,6732],{"class":86},"\"http:\u002F\u002Flocalhost:8000\"",[76,6734,1919],{"class":1549},[76,6736,6737],{"class":2267}," name",[76,6739,112],{"class":111},[76,6741,5670],{"class":86},[76,6743,1764],{"class":1549},[76,6745,6746,6749,6752,6754,6757,6759,6762,6764,6767,6769,6772,6774,6777],{"class":78,"line":1702},[76,6747,6748],{"class":103},"await",[76,6750,6751],{"class":107}," local_agent",[76,6753,1354],{"class":1549},[76,6755,6756],{"class":1754},"a_initiate_chat",[76,6758,1758],{"class":1549},[76,6760,6761],{"class":2267},"recipient",[76,6763,112],{"class":111},[76,6765,6766],{"class":107},"remote",[76,6768,1919],{"class":1549},[76,6770,6771],{"class":2267}," message",[76,6773,112],{"class":111},[76,6775,6776],{"class":86},"\"Write a CSV parser\"",[76,6778,1764],{"class":1549},[15,6780,6781],{},"A2A и MCP — не конкуренты, а комплементарные протоколы. MCP даёт агенту инструменты, A2A даёт агенту коллег. Вместе они формируют полный стек для мультиагентных систем.",[22,6783,6785],{"id":6784},"сводная-таблица","Сводная таблица",[1265,6787,6788,6808],{},[1268,6789,6790],{},[1271,6791,6792,6795,6798,6800,6802,6804,6806],{},[1274,6793,6794],{},"Фича",[1274,6796,6797],{},"Hermes",[1274,6799,5473],{},[1274,6801,5425],{},[1274,6803,5442],{},[1274,6805,5458],{},[1274,6807,5488],{},[1287,6809,6810,6830,6850,6867,6883,6899,6915,6933,6950,6967],{},[1271,6811,6812,6815,6818,6820,6822,6825,6827],{},[1292,6813,6814],{},"Оркестрация",[1292,6816,6817],{},"multi",[1292,6819,6817],{},[1292,6821,6817],{},[1292,6823,6824],{},"parallel",[1292,6826,6817],{},[1292,6828,6829],{},"single",[1271,6831,6832,6835,6838,6841,6843,6845,6847],{},[1292,6833,6834],{},"Shared memory",[1292,6836,6837],{},"partial",[1292,6839,6840],{},"✅",[1292,6842,6840],{},[1292,6844,6840],{},[1292,6846,6840],{},[1292,6848,6849],{},"❌",[1271,6851,6852,6855,6857,6859,6861,6863,6865],{},[1292,6853,6854],{},"Adversarial debate",[1292,6856,6837],{},[1292,6858,6840],{},[1292,6860,6849],{},[1292,6862,6849],{},[1292,6864,6840],{},[1292,6866,6849],{},[1271,6868,6869,6871,6873,6875,6877,6879,6881],{},[1292,6870,5578],{},[1292,6872,6849],{},[1292,6874,6837],{},[1292,6876,6849],{},[1292,6878,6849],{},[1292,6880,6840],{},[1292,6882,6849],{},[1271,6884,6885,6887,6889,6891,6893,6895,6897],{},[1292,6886,5588],{},[1292,6888,6837],{},[1292,6890,6849],{},[1292,6892,6840],{},[1292,6894,6840],{},[1292,6896,6849],{},[1292,6898,6849],{},[1271,6900,6901,6903,6905,6907,6909,6911,6913],{},[1292,6902,5598],{},[1292,6904,6837],{},[1292,6906,6837],{},[1292,6908,6840],{},[1292,6910,6840],{},[1292,6912,6849],{},[1292,6914,6849],{},[1271,6916,6917,6920,6922,6925,6927,6929,6931],{},[1292,6918,6919],{},"A2A протокол",[1292,6921,6837],{},[1292,6923,6924],{},"✅ native",[1292,6926,6837],{},[1292,6928,6837],{},[1292,6930,6849],{},[1292,6932,6849],{},[1271,6934,6935,6938,6940,6942,6944,6946,6948],{},[1292,6936,6937],{},"CLI",[1292,6939,6840],{},[1292,6941,6849],{},[1292,6943,6849],{},[1292,6945,6849],{},[1292,6947,6849],{},[1292,6949,6849],{},[1271,6951,6952,6955,6957,6959,6961,6963,6965],{},[1292,6953,6954],{},"Persistent profiles",[1292,6956,6840],{},[1292,6958,6849],{},[1292,6960,6849],{},[1292,6962,6849],{},[1292,6964,6849],{},[1292,6966,6849],{},[1271,6968,6969,6972,6974,6976,6978,6980,6982],{},[1292,6970,6971],{},"Skills\u002Fпамять",[1292,6973,6840],{},[1292,6975,6849],{},[1292,6977,6849],{},[1292,6979,6849],{},[1292,6981,6849],{},[1292,6983,6849],{},[22,6985,6987],{"id":6986},"рекомендации","Рекомендации",[15,6989,6990,6993],{},[478,6991,6992],{},"Быстрый старт с ролевыми командами"," → CrewAI. Простой API, managed runtime, MIT-лицения. Для MVP и прототипов — идеально.",[15,6995,6996,6999],{},[478,6997,6998],{},"Сложные stateful-пайплайны"," → LangGraph. DAG с чекпоинтами, conditional routing, human-in-the-loop. Если твой workflow имеет ветвления и нужен откат — это оно.",[15,7001,7002,7005],{},[478,7003,7004],{},"A2A-интероп и гибкая оркестрация"," → AG2. Единственный крупный фреймворк с нативной A2A v1.0. Пять паттернов координации. Если строишь систему, где агенты из разных фреймворков должны общаться — AG2.",[15,7007,7008,7011],{},[478,7009,7010],{},"Исследование и эксперименты"," → CAMEL-AI. RolePlaying, Inception Prompting, академический подход. Для изучения того, как LLM-агенты спорят и обучаются — лучший выбор.",[15,7013,7014,7017],{},[478,7015,7016],{},"Платформа для persistent агентов"," → Hermes Agent. CLI, Kanban, profiles, skills, holographic memory. Не фреймворк для написания агентов, а операционная система для их запуска.",[15,7019,7020,7023],{},[478,7021,7022],{},"Простые эксперименты"," → OpenPlanter. Минимальный каркас, ничего лишнего.",[22,7025,7027],{"id":7026},"что-будет-дальше","Что будет дальше",[15,7029,7030],{},"Мультиагентные фреймворки движутся в сторону интероперабельности. A2A станет стандартом де-факто — слишком много крупных игроков уже его поддерживают. MCP + A2A дадут полный стек: инструменты для агента и агенты для агента.",[15,7032,7033],{},"Hermes Agent выделяется тем, что решает проблему на другом уровне. Пока другие фреймворки спорят о паттернах оркестрации, Hermes создаёт инфраструктуру для persistent агентов с памятью, навыками и задачами. Это как разница между «библиотекой для HTTP-запросов» и «веб-сервером с плагинами».",[15,7035,7036],{},"AG2 тихо стал самым зрелым фреймворком для production-мультиагентных систем. Пять паттернов координации, нативный A2A, context variables. Если Microsoft вернёт себе AutoGen-бренд или ag2ai получит серьёзное финансирование — CrewAI и LangGraph могут оказаться под давлением.",[15,7038,7039],{},"CAMEL-AI остаётся исследовательским проектом, и это нормально. Не всё должно быть production-ready. Inception Prompting и RolePlaying — это будущее, которое пока живёт в лаборатории.",[15,7041,7042],{},"Выбирай фреймворк под задачу, а не под хайп. И помни: мультиагентная система — это не серебряная пуля. Если один агент справляется с задачей, не добавляйте ещё четырёх «для красоты».",[652,7044,7045],{},"html pre.shiki code .saXKZ, html code.shiki .saXKZ{--shiki-light:#D73A49;--shiki-dark:#CBA6F7}html pre.shiki code .slTIY, html code.shiki .slTIY{--shiki-light:#24292E;--shiki-dark:#CDD6F4}html pre.shiki code .s_QEy, html code.shiki .s_QEy{--shiki-light:#24292E;--shiki-dark:#9399B2}html pre.shiki code .s_Q3D, html code.shiki .s_Q3D{--shiki-light:#D73A49;--shiki-dark:#94E2D5}html pre.shiki code .sPNDc, html code.shiki .sPNDc{--shiki-light:#24292E;--shiki-dark:#89B4FA}html pre.shiki code .sG7gF, html code.shiki .sG7gF{--shiki-light:#032F62;--shiki-dark:#A6E3A1}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 .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 .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 .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 .sdETa, html code.shiki .sdETa{--shiki-light:#032F62;--shiki-light-font-style:inherit;--shiki-dark:#A6E3A1;--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}",{"title":71,"searchDepth":118,"depth":118,"links":7047},[7048,7049,7050,7059,7067,7068,7069,7070],{"id":5366,"depth":118,"text":5367},{"id":5379,"depth":118,"text":5380},{"id":5505,"depth":118,"text":5506,"children":7051},[7052,7053,7054,7055,7056,7057,7058],{"id":5512,"depth":349,"text":5513},{"id":5537,"depth":349,"text":5538},{"id":5554,"depth":349,"text":5555},{"id":5564,"depth":349,"text":5565},{"id":5577,"depth":349,"text":5578},{"id":5587,"depth":349,"text":5588},{"id":5597,"depth":349,"text":5598},{"id":5604,"depth":118,"text":5605,"children":7060},[7061,7062,7063,7064,7065,7066],{"id":5608,"depth":349,"text":5609},{"id":5843,"depth":349,"text":5425},{"id":6122,"depth":349,"text":5442},{"id":5461,"depth":349,"text":5458},{"id":6511,"depth":349,"text":5408},{"id":6531,"depth":349,"text":5488},{"id":6540,"depth":118,"text":6541},{"id":6784,"depth":118,"text":6785},{"id":6986,"depth":118,"text":6987},{"id":7026,"depth":118,"text":7027},"2026-06-01","Сравнение шести фреймворков для мультиагентных AI-систем — AG2, CrewAI, LangGraph, CAMEL-AI, Hermes Agent и OpenPlanter. Архитектура, фичи, реальные отличия.",{},"\u002Fblog\u002Fmulti-agent-frameworks-comparison",{"title":5361,"description":7072},"blog\u002Fmulti-agent-frameworks-comparison",[7078,7079,7080,7081,7082,7083,5843,6122,7084,5461,6511],"multi-agent","ai","frameworks","comparison","a2a","orchestration","ag2","4gARHD1CmsDy91fDFcNlEe7dcnEqxf7izD9IPiyjj8I",{"id":7087,"title":7088,"body":7089,"date":7071,"description":8611,"extension":673,"meta":8612,"navigation":345,"path":8613,"readingTime":676,"seo":8614,"stem":8615,"tags":8616,"__hash__":8617},"articles\u002Fblog\u002Fmulti-agent-frameworks-comparison.en.md","Multi-Agent Frameworks: Who Orchestrates Whom",{"type":8,"value":7090,"toc":8586},[7091,7095,7098,7101,7104,7108,7203,7206,7210,7213,7217,7220,7225,7230,7235,7239,7242,7248,7251,7255,7258,7261,7265,7268,7271,7274,7277,7280,7283,7286,7289,7292,7295,7298,7302,7304,7307,7310,7313,7316,7319,7492,7501,7507,7509,7512,7515,7741,7744,7747,7752,7754,7757,7760,8066,8069,8072,8075,8080,8082,8085,8088,8091,8096,8098,8101,8104,8107,8110,8115,8117,8120,8123,8127,8130,8133,8136,8139,8332,8335,8339,8525,8529,8535,8541,8547,8553,8559,8565,8569,8572,8575,8578,8581,8584],[22,7092,7094],{"id":7093},"why-multi-agent-systems-matter","Why Multi-Agent Systems Matter",[15,7096,7097],{},"Imagine assigning one person to write an article, conduct research, create illustrations, and edit the text. They'll get it done — slowly, with constant context switching, but they'll manage. Now imagine assigning each of those four tasks to a dedicated specialist, working in parallel. The result will be faster and likely higher quality.",[15,7099,7100],{},"Multi-agent AI systems operate on the same principle. Instead of a single LLM agent doing everything, you deploy multiple agents with distinct roles. One searches for information, another writes code, a third validates the output. The framework handles orchestration — who does what, how they communicate, and what happens when something goes wrong.",[15,7102,7103],{},"In 2025–2026, this idea moved from experimental prototypes into production. There are dozens of frameworks on GitHub, each with its own philosophy. I selected six that are actually used in practice and dissected them thoroughly.",[22,7105,7107],{"id":7106},"what-were-comparing","What We're Comparing",[1265,7109,7110,7129],{},[1268,7111,7112],{},[1271,7113,7114,7117,7120,7123,7126],{},[1274,7115,7116],{},"Framework",[1274,7118,7119],{},"Author",[1274,7121,7122],{},"Stars",[1274,7124,7125],{},"License",[1274,7127,7128],{},"Latest Release",[1287,7130,7131,7143,7155,7167,7179,7191],{},[1271,7132,7133,7135,7137,7139,7141],{},[1292,7134,5408],{},[1292,7136,5411],{},[1292,7138,5414],{},[1292,7140,5417],{},[1292,7142,5420],{},[1271,7144,7145,7147,7149,7151,7153],{},[1292,7146,5425],{},[1292,7148,5428],{},[1292,7150,5431],{},[1292,7152,5434],{},[1292,7154,5437],{},[1271,7156,7157,7159,7161,7163,7165],{},[1292,7158,5442],{},[1292,7160,5445],{},[1292,7162,5448],{},[1292,7164,5434],{},[1292,7166,5453],{},[1271,7168,7169,7171,7173,7175,7177],{},[1292,7170,5458],{},[1292,7172,5461],{},[1292,7174,5464],{},[1292,7176,5417],{},[1292,7178,5420],{},[1271,7180,7181,7183,7185,7187,7189],{},[1292,7182,5473],{},[1292,7184,5476],{},[1292,7186,5479],{},[1292,7188,5417],{},[1292,7190,5420],{},[1271,7192,7193,7195,7197,7199,7201],{},[1292,7194,5488],{},[1292,7196,5491],{},[1292,7198,5494],{},[1292,7200,5434],{},[1292,7202,5499],{},[15,7204,7205],{},"All are Python-based. All are open source. But that's where the similarities end.",[22,7207,7209],{"id":7208},"key-features-and-what-they-mean","Key Features (And What They Mean)",[15,7211,7212],{},"Before diving into comparisons, let's clarify the terminology. The same words appear across framework docs and comparison tables, but they often mean very different things.",[137,7214,7216],{"id":7215},"orchestration-single-multi-parallel","Orchestration (Single \u002F Multi \u002F Parallel)",[15,7218,7219],{},"This is the heart of any multi-agent framework — how agents coordinate their work.",[15,7221,7222,7224],{},[478,7223,5521],{}," — one agent, one task. Essentially a standard LLM call. OpenPlanter works exactly this way: you run a chain, but there are no truly parallel agents inside it.",[15,7226,7227,7229],{},[478,7228,5527],{}," — multiple agents, but they take turns or follow a leader-follower pattern. AG2, CrewAI, and CAMEL-AI use this approach. One agent can delegate a task to another, but they don't pull simultaneously.",[15,7231,7232,7234],{},[478,7233,5533],{}," — agents genuinely work in parallel, with checkpoints and conditional routing. LangGraph leads here: its DAG (Directed Acyclic Graph) allows branching and merging execution flows. Hermes Agent also supports multi-agent coordination via a Kanban board with task dependencies.",[137,7236,7238],{"id":7237},"shared-memory","Shared Memory",[15,7240,7241],{},"Can agents see each other's context? This is critical for complex pipelines where one agent's output becomes another's input.",[15,7243,7244,7245,7247],{},"AG2 implements this via ",[73,7246,5547],{}," — a shared dictionary that all agents in a Group Chat read from and write to. CrewAI and LangGraph also support shared state. CAMEL-AI provides shared memory through its Workforce pattern.",[15,7249,7250],{},"Hermes Agent has partial shared memory — issue #377 proposes a scratchpad pattern, but it remains open. Kanban tasks enable data exchange via metadata and comments, but that's not the same as a real-time shared memory pool.",[137,7252,7254],{"id":7253},"communication","Communication",[15,7256,7257],{},"How do agents talk to each other? All six frameworks use internal communication — messages are passed within the framework, not through an external API. This works fine for most cases, but becomes a problem when you need to connect agents across different systems.",[15,7259,7260],{},"That's where the A2A protocol comes in (more on that below).",[137,7262,7264],{"id":7263},"adversarial-debate","Adversarial Debate",[15,7266,7267],{},"One agent generates a response, another critiques it. If the critic finds issues, the first agent revises. This is a powerful pattern for improving quality: essentially code review for LLM output.",[15,7269,7270],{},"AG2 implements this through Group Chat with LLM handoffs. CAMEL-AI goes further — its RolePlaying pattern was designed from the start for adversarial debate. Hermes Agent has partial support via PR #20158 (Adversarial Debate Mode).",[15,7272,7273],{},"CrewAI, LangGraph, and OpenPlanter lack built-in debate. You can emulate it with additional agents and custom prompts, but it doesn't work out of the box.",[137,7275,7276],{"id":5577},"Inception Prompting",[15,7278,7279],{},"Self-improving prompts — an agent analyzes its own output and adjusts its system prompt for the next attempt. Sounds like sci-fi, but CAMEL-AI implements this through Inception Prompting from its original research paper (NeurIPS 2023).",[15,7281,7282],{},"AG2 has partial support via system messages. None of the others offer this.",[137,7284,7285],{"id":5587},"Adaptive Retry",[15,7287,7288],{},"What happens when an agent makes a mistake? The simple option is to retry the same request. The smart option is to analyze the error, switch models, adjust the prompt, and only then retry.",[15,7290,7291],{},"Hermes Agent plans Adaptive Retry with model escalation ladder (issue #30587, PR #30620). CrewAI and LangGraph have retry mechanisms. AG2, CAMEL-AI, and OpenPlanter do not.",[137,7293,7294],{"id":5597},"Managed Runtime",[15,7296,7297],{},"Do you need to manage the infrastructure your agents run on? CrewAI and LangGraph offer managed solutions — cloud environments where agents are pre-deployed and configured. AG2 and Hermes Agent are partial: you can self-host, but there are some runtime management contracts. CAMEL-AI and OpenPlanter are fully self-hosted.",[22,7299,7301],{"id":7300},"framework-by-framework","Framework by Framework",[137,7303,5609],{"id":5608},[15,7305,7306],{},"AG2 is the rebranded AutoGen from Microsoft, now living under the ag2ai organization. It's the most mature framework in terms of orchestration.",[15,7308,7309],{},"AG2 offers five coordination patterns: AutoPattern (LLM selects the next speaker), RoundRobin (circular rotation), Random (random selection), Manual (human chooses), and Default (explicit handoffs with conditions). This flexibility lets you build virtually any topology.",[15,7311,7312],{},"AG2's main advantage is native A2A support since version 0.10. You expose your agent as an A2A server, and agents from other frameworks can connect to it. As of today, AG2 is the only major framework with out-of-the-box A2A support compatible with the v1.0 specification.",[15,7314,7315],{},"Weaknesses: no quality gates (agents don't validate subordinate output quality), no persistent profiles, no CLI interface — only a Python library. If you need \"agent as a service\" with CLI and plugins, AG2 isn't the answer.",[15,7317,7318],{},"Here's what orchestration looks like in AG2:",[66,7320,7322],{"className":1527,"code":7321,"language":1529,"meta":71,"style":71},"from autogen import ConversableAgent, GroupChat, GroupChatManager\n\ncoder = ConversableAgent(\"coder\", system_message=\"You write Python code.\")\nreviewer = ConversableAgent(\"reviewer\", system_message=\"You review code and find bugs.\")\nwriter = ConversableAgent(\"writer\", system_message=\"You write documentation.\")\n\ngroupchat = GroupChat(\n    agents=[coder, reviewer, writer],\n    messages=[],\n    speaker_selection_method=\"auto\",  # LLM chooses who speaks next\n)\nmanager = GroupChatManager(groupchat=groupchat)\n",[73,7323,7324,7342,7346,7369,7392,7415,7419,7429,7449,7457,7470,7474],{"__ignoreMap":71},[76,7325,7326,7328,7330,7332,7334,7336,7338,7340],{"class":78,"line":79},[76,7327,5634],{"class":103},[76,7329,5637],{"class":107},[76,7331,1740],{"class":103},[76,7333,5642],{"class":107},[76,7335,1919],{"class":1549},[76,7337,5647],{"class":107},[76,7339,1919],{"class":1549},[76,7341,5652],{"class":107},[76,7343,7344],{"class":78,"line":118},[76,7345,346],{"emptyLinePlaceholder":345},[76,7347,7348,7350,7352,7354,7356,7358,7360,7362,7364,7367],{"class":78,"line":349},[76,7349,5661],{"class":107},[76,7351,112],{"class":111},[76,7353,5642],{"class":1754},[76,7355,1758],{"class":1549},[76,7357,5670],{"class":86},[76,7359,1919],{"class":1549},[76,7361,5675],{"class":2267},[76,7363,112],{"class":111},[76,7365,7366],{"class":86},"\"You write Python code.\"",[76,7368,1764],{"class":1549},[76,7370,7371,7373,7375,7377,7379,7381,7383,7385,7387,7390],{"class":78,"line":356},[76,7372,5687],{"class":107},[76,7374,112],{"class":111},[76,7376,5642],{"class":1754},[76,7378,1758],{"class":1549},[76,7380,5696],{"class":86},[76,7382,1919],{"class":1549},[76,7384,5675],{"class":2267},[76,7386,112],{"class":111},[76,7388,7389],{"class":86},"\"You review code and find bugs.\"",[76,7391,1764],{"class":1549},[76,7393,7394,7396,7398,7400,7402,7404,7406,7408,7410,7413],{"class":78,"line":365},[76,7395,5712],{"class":107},[76,7397,112],{"class":111},[76,7399,5642],{"class":1754},[76,7401,1758],{"class":1549},[76,7403,5721],{"class":86},[76,7405,1919],{"class":1549},[76,7407,5675],{"class":2267},[76,7409,112],{"class":111},[76,7411,7412],{"class":86},"\"You write documentation.\"",[76,7414,1764],{"class":1549},[76,7416,7417],{"class":78,"line":370},[76,7418,346],{"emptyLinePlaceholder":345},[76,7420,7421,7423,7425,7427],{"class":78,"line":376},[76,7422,5741],{"class":107},[76,7424,112],{"class":111},[76,7426,5647],{"class":1754},[76,7428,5748],{"class":1549},[76,7430,7431,7433,7435,7437,7439,7441,7443,7445,7447],{"class":78,"line":1628},[76,7432,5753],{"class":2267},[76,7434,112],{"class":111},[76,7436,1557],{"class":1549},[76,7438,5760],{"class":107},[76,7440,1919],{"class":1549},[76,7442,5765],{"class":107},[76,7444,1919],{"class":1549},[76,7446,5770],{"class":107},[76,7448,5773],{"class":1549},[76,7450,7451,7453,7455],{"class":78,"line":1636},[76,7452,5778],{"class":2267},[76,7454,112],{"class":111},[76,7456,5783],{"class":1549},[76,7458,7459,7461,7463,7465,7467],{"class":78,"line":1645},[76,7460,5788],{"class":2267},[76,7462,112],{"class":111},[76,7464,5793],{"class":86},[76,7466,1919],{"class":1549},[76,7468,7469],{"class":352},"  # LLM chooses who speaks next\n",[76,7471,7472],{"class":78,"line":1656},[76,7473,1764],{"class":1549},[76,7475,7476,7478,7480,7482,7484,7486,7488,7490],{"class":78,"line":1668},[76,7477,5807],{"class":107},[76,7479,112],{"class":111},[76,7481,5812],{"class":1754},[76,7483,1758],{"class":1549},[76,7485,5817],{"class":2267},[76,7487,112],{"class":111},[76,7489,5817],{"class":107},[76,7491,1764],{"class":1549},[15,7493,7494,7495,7497,7498,7500],{},"DefaultPattern gives even more control — explicit transition conditions between agents using ",[73,7496,5829],{}," and ",[73,7499,5833],{},". It's like a state machine, except agents replace states.",[15,7502,7503,7506],{},[478,7504,7505],{},"When to choose:"," You need flexible orchestration with A2A interoperability across different frameworks.",[137,7508,5425],{"id":5843},[15,7510,7511],{},"CrewAI is the most popular framework after Hermes. 52k stars, MIT license, simple API. Its philosophy centers on \"role-based teams\": you define agents with roles (researcher, writer, analyst), assign them tasks, and CrewAI orchestrates execution.",[15,7513,7514],{},"CrewAI looks roughly like this:",[66,7516,7517],{"className":1527,"code":5852,"language":1529,"meta":71,"style":71},[73,7518,7519,7537,7541,7551,7561,7571,7581,7585,7595,7605,7615,7625,7629,7633,7659,7685,7689,7727],{"__ignoreMap":71},[76,7520,7521,7523,7525,7527,7529,7531,7533,7535],{"class":78,"line":79},[76,7522,5634],{"class":103},[76,7524,5861],{"class":107},[76,7526,1740],{"class":103},[76,7528,5866],{"class":107},[76,7530,1919],{"class":1549},[76,7532,5871],{"class":107},[76,7534,1919],{"class":1549},[76,7536,5876],{"class":107},[76,7538,7539],{"class":78,"line":118},[76,7540,346],{"emptyLinePlaceholder":345},[76,7542,7543,7545,7547,7549],{"class":78,"line":349},[76,7544,5885],{"class":107},[76,7546,112],{"class":111},[76,7548,5866],{"class":1754},[76,7550,5748],{"class":1549},[76,7552,7553,7555,7557,7559],{"class":78,"line":356},[76,7554,5896],{"class":2267},[76,7556,112],{"class":111},[76,7558,5901],{"class":86},[76,7560,2010],{"class":1549},[76,7562,7563,7565,7567,7569],{"class":78,"line":365},[76,7564,5908],{"class":2267},[76,7566,112],{"class":111},[76,7568,5913],{"class":86},[76,7570,2010],{"class":1549},[76,7572,7573,7575,7577,7579],{"class":78,"line":370},[76,7574,5920],{"class":2267},[76,7576,112],{"class":111},[76,7578,5925],{"class":86},[76,7580,2010],{"class":1549},[76,7582,7583],{"class":78,"line":376},[76,7584,1764],{"class":1549},[76,7586,7587,7589,7591,7593],{"class":78,"line":1628},[76,7588,5712],{"class":107},[76,7590,112],{"class":111},[76,7592,5866],{"class":1754},[76,7594,5748],{"class":1549},[76,7596,7597,7599,7601,7603],{"class":78,"line":1636},[76,7598,5896],{"class":2267},[76,7600,112],{"class":111},[76,7602,5950],{"class":86},[76,7604,2010],{"class":1549},[76,7606,7607,7609,7611,7613],{"class":78,"line":1645},[76,7608,5908],{"class":2267},[76,7610,112],{"class":111},[76,7612,5961],{"class":86},[76,7614,2010],{"class":1549},[76,7616,7617,7619,7621,7623],{"class":78,"line":1656},[76,7618,5920],{"class":2267},[76,7620,112],{"class":111},[76,7622,5972],{"class":86},[76,7624,2010],{"class":1549},[76,7626,7627],{"class":78,"line":1668},[76,7628,1764],{"class":1549},[76,7630,7631],{"class":78,"line":1678},[76,7632,346],{"emptyLinePlaceholder":345},[76,7634,7635,7637,7639,7641,7643,7645,7647,7649,7651,7653,7655,7657],{"class":78,"line":1686},[76,7636,5987],{"class":107},[76,7638,112],{"class":111},[76,7640,5871],{"class":1754},[76,7642,1758],{"class":1549},[76,7644,5996],{"class":2267},[76,7646,112],{"class":111},[76,7648,6001],{"class":86},[76,7650,1919],{"class":1549},[76,7652,6006],{"class":2267},[76,7654,112],{"class":111},[76,7656,6011],{"class":107},[76,7658,1764],{"class":1549},[76,7660,7661,7663,7665,7667,7669,7671,7673,7675,7677,7679,7681,7683],{"class":78,"line":1691},[76,7662,6018],{"class":107},[76,7664,112],{"class":111},[76,7666,5871],{"class":1754},[76,7668,1758],{"class":1549},[76,7670,5996],{"class":2267},[76,7672,112],{"class":111},[76,7674,6031],{"class":86},[76,7676,1919],{"class":1549},[76,7678,6006],{"class":2267},[76,7680,112],{"class":111},[76,7682,6040],{"class":107},[76,7684,1764],{"class":1549},[76,7686,7687],{"class":78,"line":1702},[76,7688,346],{"emptyLinePlaceholder":345},[76,7690,7691,7693,7695,7697,7699,7701,7703,7705,7707,7709,7711,7713,7715,7717,7719,7721,7723,7725],{"class":78,"line":1708},[76,7692,6051],{"class":107},[76,7694,112],{"class":111},[76,7696,6056],{"class":1754},[76,7698,1758],{"class":1549},[76,7700,6061],{"class":2267},[76,7702,112],{"class":111},[76,7704,1557],{"class":1549},[76,7706,6011],{"class":107},[76,7708,1919],{"class":1549},[76,7710,5770],{"class":107},[76,7712,6074],{"class":1549},[76,7714,6077],{"class":2267},[76,7716,112],{"class":111},[76,7718,1557],{"class":1549},[76,7720,6084],{"class":107},[76,7722,1919],{"class":1549},[76,7724,6089],{"class":107},[76,7726,3367],{"class":1549},[76,7728,7729,7731,7733,7735,7737,7739],{"class":78,"line":1717},[76,7730,6096],{"class":107},[76,7732,112],{"class":111},[76,7734,6101],{"class":107},[76,7736,1354],{"class":1549},[76,7738,6106],{"class":1754},[76,7740,1821],{"class":1549},[15,7742,7743],{},"Two modes: sequential (tasks execute one after another) and hierarchical (manager delegates to workers). Managed runtime available — you can run it in CrewAI's cloud. Adaptive retry is built in.",[15,7745,7746],{},"Weaknesses: no adversarial debate, limited support for complex DAGs (this isn't LangGraph), restricted state persistence. A2A support exists only through community adapters; there's no native implementation.",[15,7748,7749,7751],{},[478,7750,7505],{}," You need a quick start with an intuitive role-based model. Typical use case: \"researcher finds information, writer produces a report, reviewer checks it.\"",[137,7753,5442],{"id":6122},[15,7755,7756],{},"LangGraph is a layer on top of LangChain that transforms call chains into a graph. Each node is a processing step; edges define transition conditions. You can branch, merge, checkpoint, and insert human-in-the-loop approval points.",[15,7758,7759],{},"Here's a schematic view of what the graph looks like:",[66,7761,7762],{"className":1527,"code":6131,"language":1529,"meta":71,"style":71},[73,7763,7764,7778,7782,7794,7824,7828,7840,7870,7874,7886,7912,7934,7938,7952,7970,7988,8006,8024],{"__ignoreMap":71},[76,7765,7766,7768,7770,7772,7774,7776],{"class":78,"line":79},[76,7767,5634],{"class":103},[76,7769,6140],{"class":107},[76,7771,1354],{"class":1549},[76,7773,6145],{"class":107},[76,7775,1740],{"class":103},[76,7777,6150],{"class":107},[76,7779,7780],{"class":78,"line":118},[76,7781,346],{"emptyLinePlaceholder":345},[76,7783,7784,7786,7788,7790,7792],{"class":78,"line":349},[76,7785,1578],{"class":103},[76,7787,6161],{"class":82},[76,7789,1758],{"class":1549},[76,7791,6166],{"class":1553},[76,7793,2404],{"class":1549},[76,7795,7796,7798,7800,7802,7804,7806,7808,7810,7812,7814,7816,7818,7820,7822],{"class":78,"line":356},[76,7797,1681],{"class":103},[76,7799,6175],{"class":1549},[76,7801,6178],{"class":86},[76,7803,1550],{"class":1549},[76,7805,6183],{"class":1754},[76,7807,1758],{"class":1549},[76,7809,6188],{"class":86},[76,7811,2360],{"class":111},[76,7813,6193],{"class":1553},[76,7815,1557],{"class":1549},[76,7817,441],{"class":86},[76,7819,6201],{"class":6200},[76,7821,441],{"class":86},[76,7823,6206],{"class":1549},[76,7825,7826],{"class":78,"line":365},[76,7827,346],{"emptyLinePlaceholder":345},[76,7829,7830,7832,7834,7836,7838],{"class":78,"line":370},[76,7831,1578],{"class":103},[76,7833,6217],{"class":82},[76,7835,1758],{"class":1549},[76,7837,6166],{"class":1553},[76,7839,2404],{"class":1549},[76,7841,7842,7844,7846,7848,7850,7852,7854,7856,7858,7860,7862,7864,7866,7868],{"class":78,"line":376},[76,7843,1681],{"class":103},[76,7845,6175],{"class":1549},[76,7847,6232],{"class":86},[76,7849,1550],{"class":1549},[76,7851,6183],{"class":1754},[76,7853,1758],{"class":1549},[76,7855,6241],{"class":86},[76,7857,2360],{"class":111},[76,7859,6193],{"class":1553},[76,7861,1557],{"class":1549},[76,7863,441],{"class":86},[76,7865,6252],{"class":6200},[76,7867,441],{"class":86},[76,7869,6206],{"class":1549},[76,7871,7872],{"class":78,"line":1628},[76,7873,346],{"emptyLinePlaceholder":345},[76,7875,7876,7878,7880,7882,7884],{"class":78,"line":1636},[76,7877,1578],{"class":103},[76,7879,6267],{"class":82},[76,7881,1758],{"class":1549},[76,7883,6166],{"class":1553},[76,7885,2404],{"class":1549},[76,7887,7888,7890,7892,7894,7896,7898,7900,7902,7904,7906,7908,7910],{"class":78,"line":1645},[76,7889,6278],{"class":107},[76,7891,112],{"class":111},[76,7893,6183],{"class":1754},[76,7895,1758],{"class":1549},[76,7897,6287],{"class":86},[76,7899,2360],{"class":111},[76,7901,6193],{"class":1553},[76,7903,1557],{"class":1549},[76,7905,441],{"class":86},[76,7907,6298],{"class":6200},[76,7909,441],{"class":86},[76,7911,3367],{"class":1549},[76,7913,7914,7916,7918,7920,7922,7924,7926,7928,7930,7932],{"class":78,"line":1656},[76,7915,1681],{"class":103},[76,7917,6175],{"class":1549},[76,7919,6311],{"class":86},[76,7921,1550],{"class":1549},[76,7923,6316],{"class":86},[76,7925,6319],{"class":103},[76,7927,6322],{"class":107},[76,7929,1354],{"class":1549},[76,7931,6327],{"class":1754},[76,7933,6330],{"class":1549},[76,7935,7936],{"class":78,"line":1668},[76,7937,346],{"emptyLinePlaceholder":345},[76,7939,7940,7942,7944,7946,7948,7950],{"class":78,"line":1678},[76,7941,6145],{"class":107},[76,7943,112],{"class":111},[76,7945,6343],{"class":1754},[76,7947,1758],{"class":1549},[76,7949,6348],{"class":1560},[76,7951,1764],{"class":1549},[76,7953,7954,7956,7958,7960,7962,7964,7966,7968],{"class":78,"line":1686},[76,7955,6355],{"class":107},[76,7957,1354],{"class":1549},[76,7959,6360],{"class":1754},[76,7961,1758],{"class":1549},[76,7963,6365],{"class":86},[76,7965,1919],{"class":1549},[76,7967,6161],{"class":107},[76,7969,1764],{"class":1549},[76,7971,7972,7974,7976,7978,7980,7982,7984,7986],{"class":78,"line":1691},[76,7973,6355],{"class":107},[76,7975,1354],{"class":1549},[76,7977,6360],{"class":1754},[76,7979,1758],{"class":1549},[76,7981,6384],{"class":86},[76,7983,1919],{"class":1549},[76,7985,6217],{"class":107},[76,7987,1764],{"class":1549},[76,7989,7990,7992,7994,7996,7998,8000,8002,8004],{"class":78,"line":1702},[76,7991,6355],{"class":107},[76,7993,1354],{"class":1549},[76,7995,6360],{"class":1754},[76,7997,1758],{"class":1549},[76,7999,6403],{"class":86},[76,8001,1919],{"class":1549},[76,8003,6267],{"class":107},[76,8005,1764],{"class":1549},[76,8007,8008,8010,8012,8014,8016,8018,8020,8022],{"class":78,"line":1708},[76,8009,6355],{"class":107},[76,8011,1354],{"class":1549},[76,8013,6418],{"class":1754},[76,8015,1758],{"class":1549},[76,8017,6365],{"class":86},[76,8019,1919],{"class":1549},[76,8021,6427],{"class":86},[76,8023,1764],{"class":1549},[76,8025,8026,8028,8030,8032,8034,8036,8038,8040,8042,8044,8046,8048,8050,8052,8054,8056,8058,8060,8062,8064],{"class":78,"line":1717},[76,8027,6355],{"class":107},[76,8029,1354],{"class":1549},[76,8031,6438],{"class":1754},[76,8033,1758],{"class":1549},[76,8035,6403],{"class":86},[76,8037,1919],{"class":1549},[76,8039,6447],{"class":103},[76,8041,6450],{"class":1553},[76,8043,1550],{"class":1549},[76,8045,6455],{"class":86},[76,8047,6458],{"class":103},[76,8049,6450],{"class":1553},[76,8051,1557],{"class":1549},[76,8053,441],{"class":86},[76,8055,6467],{"class":6200},[76,8057,441],{"class":86},[76,8059,1564],{"class":1549},[76,8061,6474],{"class":103},[76,8063,6427],{"class":86},[76,8065,1764],{"class":1549},[15,8067,8068],{},"Its strength lies in stateful workflows with checkpoints. If an agent fails at step 7 of 12, you can roll back to step 6 and continue. This is rare among multi-agent frameworks. Human-in-the-loop is also well implemented — you can insert a \"pause\" into the graph for human confirmation before proceeding.",[15,8070,8071],{},"33k stars, MIT license, managed runtime via LangGraph Cloud. Adaptive retry is supported.",[15,8073,8074],{},"Weaknesses: no built-in adversarial debate, no inception prompting, A2A only through community efforts. LangGraph focuses more on workflow orchestration than on \"agents arguing with each other.\"",[15,8076,8077,8079],{},[478,8078,7505],{}," Complex multi-step pipelines with branching, checkpoints, and human-in-the-loop. If you need a \"processing graph with rollback capability,\" LangGraph is your answer.",[137,8081,5458],{"id":5461},[15,8083,8084],{},"CAMEL-AI is the most research-oriented framework. Born as an academic project (NeurIPS 2023, arXiv:2303.17760), it retains its research DNA to this day.",[15,8086,8087],{},"CAMEL-AI offers two unique patterns: RolePlaying (two agents play roles and debate until reaching consensus) and Inception Prompting (an agent improves its prompts based on previous attempts). The Workforce pattern implements shared memory for agent teams.",[15,8089,8090],{},"17k stars, Apache 2.0 license. Downsides include no managed runtime, no adaptive retry, and no A2A support. The framework is geared more toward experimentation than production use.",[15,8092,8093,8095],{},[478,8094,7505],{}," Research tasks requiring adversarial debate and inception prompting. If you're studying how LLM agents can argue and self-improve, CAMEL-AI is the place.",[137,8097,5408],{"id":6511},[15,8099,8100],{},"Hermes Agent by NousResearch isn't quite a framework in the traditional sense. It's a CLI-first system with a Kanban board, profiles, skills, and plugins. With 164k stars, it's the most popular on this list.",[15,8102,8103],{},"The philosophy differs: Hermes addresses the application layer (tasks, memory, skills, CLI) rather than the framework layer (Python SDK for writing agents). The Kanban board with dependencies serves as orchestration: tasks block, unblock, and transfer between agent profiles.",[15,8105,8106],{},"Shared memory is partial, adversarial debate is partial (PR #20158), adaptive retry is in development (PR #30620). A2A implementation exists (PR #4135, +2831 lines, 71 tests) but hasn't been merged.",[15,8108,8109],{},"Hermes' greatest strength is its ecosystem. Persistent profiles (isolated configuration, memory, and skills per agent), Skills system (procedural memory), Holographic Memory (hybrid search combining FTS5 + semantic embeddings). No other framework offers anything comparable.",[15,8111,8112,8114],{},[478,8113,7505],{}," You need a CLI system with persistent agents, tasks, and skills. Hermes isn't a \"library for writing agents\" — it's a \"platform for running agents as services.\"",[137,8116,5488],{"id":6531},[15,8118,8119],{},"OpenPlanter is the smallest framework in this roundup. 1.6k stars, single orchestration, no shared memory, no debate, no retry. Essentially, it's a wrapper for sequential LLM calls with minimal orchestration.",[15,8121,8122],{},"I wouldn't recommend it for production. But if you need a simple scaffold for experiments, OpenPlanter could be a starting point. Sometimes \"less is more\" works.",[22,8124,8126],{"id":8125},"the-a2a-protocol-why-it-matters","The A2A Protocol: Why It Matters",[15,8128,8129],{},"Google introduced the A2A (Agent-to-Agent) protocol in April 2025. The concept is straightforward: MCP answers \"what tools are available?\" while A2A answers \"who can help?\"",[15,8131,8132],{},"Key concepts: Agent Card (JSON description of agent capabilities, analogous to MCP's tool list), Task (unit of work), Message (communication within a task), Artifact (result). Streaming via SSE is supported.",[15,8134,8135],{},"Native A2A support currently exists in: AG2 (since v0.10), Google ADK (from day one), Pydantic AI. Community adapters are available for CrewAI and LangChain. Hermes Agent has a complete implementation, but PR #4135 remains unmerged.",[15,8137,8138],{},"Why does this matter? Because multi-agent frameworks are islands. Your CrewAI agent can't easily call an AG2 agent. A2A solves this: expose your agent as an A2A server, and any framework with an A2A client can reach it.",[66,8140,8142],{"className":1527,"code":8141,"language":1529,"meta":71,"style":71},"# AG2: exposing an agent as an A2A server\nfrom autogen import ConversableAgent, LLMConfig\nfrom autogen.a2a import A2aAgentServer\n\nagent = ConversableAgent(\n    name=\"coder\",\n    system_message=\"Expert Python developer\",\n    llm_config=LLMConfig({\"model\": \"gpt-4o-mini\"}),\n)\nserver = A2aAgentServer(agent).build()\n# uvicorn server:server --port 8000\n\n# Connecting from another process\nfrom autogen.a2a import A2aRemoteAgent\nremote = A2aRemoteAgent(url=\"http:\u002F\u002Flocalhost:8000\", name=\"coder\")\nawait local_agent.a_initiate_chat(recipient=remote, message=\"Write a CSV parser\")\n",[73,8143,8144,8149,8163,8177,8181,8191,8201,8211,8229,8233,8251,8255,8259,8264,8278,8304],{"__ignoreMap":71},[76,8145,8146],{"class":78,"line":79},[76,8147,8148],{"class":352},"# AG2: exposing an agent as an A2A server\n",[76,8150,8151,8153,8155,8157,8159,8161],{"class":78,"line":118},[76,8152,5634],{"class":103},[76,8154,5637],{"class":107},[76,8156,1740],{"class":103},[76,8158,5642],{"class":107},[76,8160,1919],{"class":1549},[76,8162,6578],{"class":107},[76,8164,8165,8167,8169,8171,8173,8175],{"class":78,"line":349},[76,8166,5634],{"class":103},[76,8168,6585],{"class":107},[76,8170,1354],{"class":1549},[76,8172,6590],{"class":107},[76,8174,1740],{"class":103},[76,8176,6595],{"class":107},[76,8178,8179],{"class":78,"line":356},[76,8180,346],{"emptyLinePlaceholder":345},[76,8182,8183,8185,8187,8189],{"class":78,"line":365},[76,8184,6604],{"class":107},[76,8186,112],{"class":111},[76,8188,5642],{"class":1754},[76,8190,5748],{"class":1549},[76,8192,8193,8195,8197,8199],{"class":78,"line":370},[76,8194,6615],{"class":2267},[76,8196,112],{"class":111},[76,8198,5670],{"class":86},[76,8200,2010],{"class":1549},[76,8202,8203,8205,8207,8209],{"class":78,"line":376},[76,8204,6626],{"class":2267},[76,8206,112],{"class":111},[76,8208,6631],{"class":86},[76,8210,2010],{"class":1549},[76,8212,8213,8215,8217,8219,8221,8223,8225,8227],{"class":78,"line":1628},[76,8214,6638],{"class":2267},[76,8216,112],{"class":111},[76,8218,6643],{"class":1754},[76,8220,6646],{"class":1549},[76,8222,6649],{"class":86},[76,8224,1550],{"class":1549},[76,8226,6654],{"class":86},[76,8228,6657],{"class":1549},[76,8230,8231],{"class":78,"line":1636},[76,8232,1764],{"class":1549},[76,8234,8235,8237,8239,8241,8243,8245,8247,8249],{"class":78,"line":1645},[76,8236,6666],{"class":107},[76,8238,112],{"class":111},[76,8240,6671],{"class":1754},[76,8242,1758],{"class":1549},[76,8244,6676],{"class":107},[76,8246,2281],{"class":1549},[76,8248,6681],{"class":1754},[76,8250,1821],{"class":1549},[76,8252,8253],{"class":78,"line":1656},[76,8254,6688],{"class":352},[76,8256,8257],{"class":78,"line":1668},[76,8258,346],{"emptyLinePlaceholder":345},[76,8260,8261],{"class":78,"line":1678},[76,8262,8263],{"class":352},"# Connecting from another process\n",[76,8265,8266,8268,8270,8272,8274,8276],{"class":78,"line":1686},[76,8267,5634],{"class":103},[76,8269,6585],{"class":107},[76,8271,1354],{"class":1549},[76,8273,6590],{"class":107},[76,8275,1740],{"class":103},[76,8277,6712],{"class":107},[76,8279,8280,8282,8284,8286,8288,8290,8292,8294,8296,8298,8300,8302],{"class":78,"line":1691},[76,8281,6717],{"class":107},[76,8283,112],{"class":111},[76,8285,6722],{"class":1754},[76,8287,1758],{"class":1549},[76,8289,6727],{"class":2267},[76,8291,112],{"class":111},[76,8293,6732],{"class":86},[76,8295,1919],{"class":1549},[76,8297,6737],{"class":2267},[76,8299,112],{"class":111},[76,8301,5670],{"class":86},[76,8303,1764],{"class":1549},[76,8305,8306,8308,8310,8312,8314,8316,8318,8320,8322,8324,8326,8328,8330],{"class":78,"line":1702},[76,8307,6748],{"class":103},[76,8309,6751],{"class":107},[76,8311,1354],{"class":1549},[76,8313,6756],{"class":1754},[76,8315,1758],{"class":1549},[76,8317,6761],{"class":2267},[76,8319,112],{"class":111},[76,8321,6766],{"class":107},[76,8323,1919],{"class":1549},[76,8325,6771],{"class":2267},[76,8327,112],{"class":111},[76,8329,6776],{"class":86},[76,8331,1764],{"class":1549},[15,8333,8334],{},"A2A and MCP aren't competitors — they're complementary protocols. MCP gives agents tools; A2A gives agents colleagues. Together, they form a complete stack for multi-agent systems.",[22,8336,8338],{"id":8337},"summary-table","Summary Table",[1265,8340,8341,8360],{},[1268,8342,8343],{},[1271,8344,8345,8348,8350,8352,8354,8356,8358],{},[1274,8346,8347],{},"Feature",[1274,8349,6797],{},[1274,8351,5473],{},[1274,8353,5425],{},[1274,8355,5442],{},[1274,8357,5458],{},[1274,8359,5488],{},[1287,8361,8362,8379,8395,8411,8427,8443,8459,8476,8492,8508],{},[1271,8363,8364,8367,8369,8371,8373,8375,8377],{},[1292,8365,8366],{},"Orchestration",[1292,8368,6817],{},[1292,8370,6817],{},[1292,8372,6817],{},[1292,8374,6824],{},[1292,8376,6817],{},[1292,8378,6829],{},[1271,8380,8381,8383,8385,8387,8389,8391,8393],{},[1292,8382,6834],{},[1292,8384,6837],{},[1292,8386,6840],{},[1292,8388,6840],{},[1292,8390,6840],{},[1292,8392,6840],{},[1292,8394,6849],{},[1271,8396,8397,8399,8401,8403,8405,8407,8409],{},[1292,8398,6854],{},[1292,8400,6837],{},[1292,8402,6840],{},[1292,8404,6849],{},[1292,8406,6849],{},[1292,8408,6840],{},[1292,8410,6849],{},[1271,8412,8413,8415,8417,8419,8421,8423,8425],{},[1292,8414,5578],{},[1292,8416,6849],{},[1292,8418,6837],{},[1292,8420,6849],{},[1292,8422,6849],{},[1292,8424,6840],{},[1292,8426,6849],{},[1271,8428,8429,8431,8433,8435,8437,8439,8441],{},[1292,8430,5588],{},[1292,8432,6837],{},[1292,8434,6849],{},[1292,8436,6840],{},[1292,8438,6840],{},[1292,8440,6849],{},[1292,8442,6849],{},[1271,8444,8445,8447,8449,8451,8453,8455,8457],{},[1292,8446,5598],{},[1292,8448,6837],{},[1292,8450,6837],{},[1292,8452,6840],{},[1292,8454,6840],{},[1292,8456,6849],{},[1292,8458,6849],{},[1271,8460,8461,8464,8466,8468,8470,8472,8474],{},[1292,8462,8463],{},"A2A protocol",[1292,8465,6837],{},[1292,8467,6924],{},[1292,8469,6837],{},[1292,8471,6837],{},[1292,8473,6849],{},[1292,8475,6849],{},[1271,8477,8478,8480,8482,8484,8486,8488,8490],{},[1292,8479,6937],{},[1292,8481,6840],{},[1292,8483,6849],{},[1292,8485,6849],{},[1292,8487,6849],{},[1292,8489,6849],{},[1292,8491,6849],{},[1271,8493,8494,8496,8498,8500,8502,8504,8506],{},[1292,8495,6954],{},[1292,8497,6840],{},[1292,8499,6849],{},[1292,8501,6849],{},[1292,8503,6849],{},[1292,8505,6849],{},[1292,8507,6849],{},[1271,8509,8510,8513,8515,8517,8519,8521,8523],{},[1292,8511,8512],{},"Skills\u002Fmemory",[1292,8514,6840],{},[1292,8516,6849],{},[1292,8518,6849],{},[1292,8520,6849],{},[1292,8522,6849],{},[1292,8524,6849],{},[22,8526,8528],{"id":8527},"recommendations","Recommendations",[15,8530,8531,8534],{},[478,8532,8533],{},"Quick start with role-based teams"," → CrewAI. Simple API, managed runtime, MIT license. Ideal for MVPs and prototypes.",[15,8536,8537,8540],{},[478,8538,8539],{},"Complex stateful pipelines"," → LangGraph. DAGs with checkpoints, conditional routing, human-in-the-loop. If your workflow involves branching and requires rollback capability, this is it.",[15,8542,8543,8546],{},[478,8544,8545],{},"A2A interoperability and flexible orchestration"," → AG2. The only major framework with native A2A v1.0 support. Five coordination patterns. If you're building a system where agents from different frameworks must communicate, choose AG2.",[15,8548,8549,8552],{},[478,8550,8551],{},"Research and experimentation"," → CAMEL-AI. RolePlaying, Inception Prompting, academic approach. Best choice for studying how LLM agents argue and learn.",[15,8554,8555,8558],{},[478,8556,8557],{},"Platform for persistent agents"," → Hermes Agent. CLI, Kanban, profiles, skills, holographic memory. Not a framework for writing agents, but an operating system for running them.",[15,8560,8561,8564],{},[478,8562,8563],{},"Simple experiments"," → OpenPlanter. Minimal scaffold, nothing extra.",[22,8566,8568],{"id":8567},"whats-next","What's Next",[15,8570,8571],{},"Multi-agent frameworks are moving toward interoperability. A2A will become the de facto standard — too many major players already support it. MCP + A2A will deliver a complete stack: tools for agents and agents for agents.",[15,8573,8574],{},"Hermes Agent stands out by solving the problem at a different level. While other frameworks debate orchestration patterns, Hermes builds infrastructure for persistent agents with memory, skills, and tasks. It's like the difference between \"an HTTP request library\" and \"a web server with plugins.\"",[15,8576,8577],{},"AG2 has quietly become the most mature framework for production multi-agent systems. Five coordination patterns, native A2A, context variables. If Microsoft reclaims the AutoGen brand or ag2ai secures significant funding, CrewAI and LangGraph could face serious pressure.",[15,8579,8580],{},"CAMEL-AI remains a research project, and that's perfectly fine. Not everything needs to be production-ready. Inception Prompting and RolePlaying represent a future that still lives in the lab.",[15,8582,8583],{},"Choose a framework based on your task, not the hype. And remember: a multi-agent system isn't a silver bullet. If one agent handles the job, don't add four more just for show.",[652,8585,7045],{},{"title":71,"searchDepth":118,"depth":118,"links":8587},[8588,8589,8590,8599,8607,8608,8609,8610],{"id":7093,"depth":118,"text":7094},{"id":7106,"depth":118,"text":7107},{"id":7208,"depth":118,"text":7209,"children":8591},[8592,8593,8594,8595,8596,8597,8598],{"id":7215,"depth":349,"text":7216},{"id":7237,"depth":349,"text":7238},{"id":7253,"depth":349,"text":7254},{"id":7263,"depth":349,"text":7264},{"id":5577,"depth":349,"text":7276},{"id":5587,"depth":349,"text":7285},{"id":5597,"depth":349,"text":7294},{"id":7300,"depth":118,"text":7301,"children":8600},[8601,8602,8603,8604,8605,8606],{"id":5608,"depth":349,"text":5609},{"id":5843,"depth":349,"text":5425},{"id":6122,"depth":349,"text":5442},{"id":5461,"depth":349,"text":5458},{"id":6511,"depth":349,"text":5408},{"id":6531,"depth":349,"text":5488},{"id":8125,"depth":118,"text":8126},{"id":8337,"depth":118,"text":8338},{"id":8527,"depth":118,"text":8528},{"id":8567,"depth":118,"text":8568},"A comparison of six multi-agent AI system frameworks — AG2, CrewAI, LangGraph, CAMEL-AI, Hermes Agent, and OpenPlanter. Architecture, features, and real-world differences.",{},"\u002Fblog\u002Fmulti-agent-frameworks-comparison.en",{"title":7088,"description":8611},"blog\u002Fmulti-agent-frameworks-comparison.en",[7078,7079,7080,7081,7082,7083,5843,6122,7084,5461,6511],"77odJT1Xse31FROA9lWALyDebA3Syyv8c5Mi0GDhs8A",{"id":8619,"title":8620,"body":8621,"date":671,"description":9965,"extension":673,"meta":9966,"navigation":345,"path":9967,"readingTime":676,"seo":9968,"stem":9969,"tags":9970,"__hash__":9974},"articles\u002Fblog\u002Fself-hosted-ntfy-notifications.md","Самостоятельный ntfy: свой push-сервер уведомлений",{"type":8,"value":8622,"toc":9929},[8623,8626,8629,8638,8642,8645,8663,8674,8678,8684,8773,8776,8788,8800,8808,8818,8857,8861,8873,8904,8911,8915,8921,8927,8930,8958,8962,8965,8986,8992,8995,9011,9021,9025,9028,9060,9063,9093,9096,9151,9155,9158,9173,9176,9190,9193,9277,9284,9288,9291,9296,9413,9419,9432,9436,9440,9453,9457,9460,9466,9469,9473,9487,9491,9494,9526,9529,9533,9537,9550,9556,9616,9620,9623,9628,9663,9667,9670,9693,9696,9700,9703,9717,9720,9732,9736,9740,9743,9793,9797,9807,9811,9814,9827,9831,9834,9868,9872,9876,9879,9883,9886,9890,9893,9897,9900,9904,9911,9914,9916,9923,9926],[11,8624,8620],{"id":8625},"самостоятельный-ntfy-свой-push-сервер-уведомлений",[15,8627,8628],{},"На моём сервере крутится AI-агент (Hermes), мониторинг, блог, и куча мелких сервисов. Мне нужно было получать уведомления от всего этого хозяйства на телефон — не через Telegram Bot API (с его rate limit и зависимостью от серверов Telegram), а через свой канал, который я контролирую.",[15,8630,8631,8632,8637],{},"Решение — ",[58,8633,8636],{"href":8634,"rel":8635},"https:\u002F\u002Fntfy.sh",[62],"ntfy",": лёгкий Go-сервер, HTTP pub-sub, Android\u002FiOS приложения, и zero vendor lock-in. Вот как я его поднял и к чему пришёл.",[22,8639,8641],{"id":8640},"установка","Установка",[15,8643,8644],{},"На Debian\u002FUbuntu всё предельно просто:",[66,8646,8648],{"className":68,"code":8647,"language":70,"meta":71,"style":71},"apt-get install -y ntfy\n",[73,8649,8650],{"__ignoreMap":71},[76,8651,8652,8655,8657,8660],{"class":78,"line":79},[76,8653,8654],{"class":82},"apt-get",[76,8656,87],{"class":86},[76,8658,8659],{"class":161}," -y",[76,8661,8662],{"class":86}," ntfy\n",[15,8664,8665,8666,8669,8670,8673],{},"Пакет создаёт пользователя ",[73,8667,8668],{},"_ntfy",", systemd-сервис и конфиг по умолчанию. Но «по умолчанию» — это listen на ",[73,8671,8672],{},":80"," и без авторизации. Для production нужно поменять.",[22,8675,8677],{"id":8676},"конфигурация","Конфигурация",[15,8679,8680,8681,1550],{},"Файл ",[73,8682,8683],{},"\u002Fetc\u002Fntfy\u002Fserver.yml",[66,8685,8689],{"className":8686,"code":8687,"language":8688,"meta":71,"style":71},"language-yaml shiki shiki-themes github-light catppuccin-mocha","base-url: \"https:\u002F\u002Fntfy.example.com\"\nlisten-http: \"127.0.0.1:8080\"\nbehind-proxy: true\ncache-file: \u002Fvar\u002Fcache\u002Fntfy\u002Fcache.db\ncache-duration: \"24h\"\nauth-file: \u002Fvar\u002Flib\u002Fntfy\u002Fuser.db\nauth-default-access: \"deny-all\"\nlog-level: info\n","yaml",[73,8690,8691,8703,8713,8723,8733,8743,8753,8763],{"__ignoreMap":71},[76,8692,8693,8697,8700],{"class":78,"line":79},[76,8694,8696],{"class":8695},"sEb-F","base-url",[76,8698,1550],{"class":8699},"sgPNX",[76,8701,8702],{"class":86}," \"https:\u002F\u002Fntfy.example.com\"\n",[76,8704,8705,8708,8710],{"class":78,"line":118},[76,8706,8707],{"class":8695},"listen-http",[76,8709,1550],{"class":8699},[76,8711,8712],{"class":86}," \"127.0.0.1:8080\"\n",[76,8714,8715,8718,8720],{"class":78,"line":349},[76,8716,8717],{"class":8695},"behind-proxy",[76,8719,1550],{"class":8699},[76,8721,8722],{"class":189}," true\n",[76,8724,8725,8728,8730],{"class":78,"line":356},[76,8726,8727],{"class":8695},"cache-file",[76,8729,1550],{"class":8699},[76,8731,8732],{"class":86}," \u002Fvar\u002Fcache\u002Fntfy\u002Fcache.db\n",[76,8734,8735,8738,8740],{"class":78,"line":365},[76,8736,8737],{"class":8695},"cache-duration",[76,8739,1550],{"class":8699},[76,8741,8742],{"class":86}," \"24h\"\n",[76,8744,8745,8748,8750],{"class":78,"line":370},[76,8746,8747],{"class":8695},"auth-file",[76,8749,1550],{"class":8699},[76,8751,8752],{"class":86}," \u002Fvar\u002Flib\u002Fntfy\u002Fuser.db\n",[76,8754,8755,8758,8760],{"class":78,"line":376},[76,8756,8757],{"class":8695},"auth-default-access",[76,8759,1550],{"class":8699},[76,8761,8762],{"class":86}," \"deny-all\"\n",[76,8764,8765,8768,8770],{"class":78,"line":1628},[76,8766,8767],{"class":8695},"log-level",[76,8769,1550],{"class":8699},[76,8771,8772],{"class":86}," info\n",[15,8774,8775],{},"Разберу ключевые моменты:",[15,8777,8778,8783,8784,8787],{},[478,8779,8780],{},[73,8781,8782],{},"listen-http: \"127.0.0.1:8080\""," — слушаем только на localhost. Весь внешний трафик идёт через reverse proxy. Если поставить ",[73,8785,8786],{},"0.0.0.0:80",", ntfy будет доступен напрямую, минуя SSL и rate limiting.",[15,8789,8790,8795,8796,8799],{},[478,8791,8792],{},[73,8793,8794],{},"behind-proxy: true"," — обязательно, если вы за Caddy\u002Fnginx. Без этого ntfy не увидит реальные IP-адреса подписчиков (будет видеть ",[73,8797,8798],{},"127.0.0.1","), и rate limiting не будет работать корректно.",[15,8801,8802,8807],{},[478,8803,8804],{},[73,8805,8806],{},"auth-default-access: \"deny-all\""," — по умолчанию всё закрыто. Без этого любой, кто угадает имя топика, может читать и писать в него. Для публичного ntfy.sh это нормально, для self-hosted — нет.",[15,8809,8810,8813,8814,8817],{},[478,8811,8812],{},"Питфолл",": не пишите конфиг через ",[73,8815,8816],{},"cat > \u002Fetc\u002Fntfy\u002Fserver.yml"," из терминала — некоторые security-сканеры блокируют heredoc в bash. Используйте Python:",[66,8819,8821],{"className":1527,"code":8820,"language":1529,"meta":71,"style":71},"import pathlib\npathlib.Path('\u002Fetc\u002Fntfy\u002Fserver.yml').write_text(config_yaml)\n",[73,8822,8823,8830],{"__ignoreMap":71},[76,8824,8825,8827],{"class":78,"line":79},[76,8826,1740],{"class":103},[76,8828,8829],{"class":107}," pathlib\n",[76,8831,8832,8835,8837,8840,8842,8845,8847,8850,8852,8855],{"class":78,"line":118},[76,8833,8834],{"class":107},"pathlib",[76,8836,1354],{"class":1549},[76,8838,8839],{"class":1754},"Path",[76,8841,1758],{"class":1549},[76,8843,8844],{"class":86},"'\u002Fetc\u002Fntfy\u002Fserver.yml'",[76,8846,2281],{"class":1549},[76,8848,8849],{"class":1754},"write_text",[76,8851,1758],{"class":1549},[76,8853,8854],{"class":107},"config_yaml",[76,8856,1764],{"class":1549},[22,8858,8860],{"id":8859},"права-на-директории","Права на директории",[15,8862,8863,8864,8866,8867,5830,8870,1550],{},"Пакетный ",[73,8865,8668],{}," пользователь должен иметь права на запись в ",[73,8868,8869],{},"\u002Fvar\u002Fcache\u002Fntfy",[73,8871,8872],{},"\u002Fvar\u002Flib\u002Fntfy",[66,8874,8876],{"className":68,"code":8875,"language":70,"meta":71,"style":71},"mkdir -p \u002Fvar\u002Fcache\u002Fntfy \u002Fvar\u002Flib\u002Fntfy\nchown _ntfy:_ntfy \u002Fvar\u002Fcache\u002Fntfy \u002Fvar\u002Flib\u002Fntfy\n",[73,8877,8878,8892],{"__ignoreMap":71},[76,8879,8880,8883,8886,8889],{"class":78,"line":79},[76,8881,8882],{"class":82},"mkdir",[76,8884,8885],{"class":161}," -p",[76,8887,8888],{"class":86}," \u002Fvar\u002Fcache\u002Fntfy",[76,8890,8891],{"class":86}," \u002Fvar\u002Flib\u002Fntfy\n",[76,8893,8894,8897,8900,8902],{"class":78,"line":118},[76,8895,8896],{"class":82},"chown",[76,8898,8899],{"class":86}," _ntfy:_ntfy",[76,8901,8888],{"class":86},[76,8903,8891],{"class":86},[15,8905,8906,8907,8910],{},"Без этого сервис упадёт с ",[73,8908,8909],{},"\"unable to open database file\"",". Классика.",[22,8912,8914],{"id":8913},"caddy-reverse-proxy","Caddy reverse proxy",[15,8916,8917,8918,8920],{},"У меня Caddy слушает на ",[73,8919,8672],{}," (Cloudflare handles SSL termination снаружи). Конфиг для ntfy:",[66,8922,8925],{"className":8923,"code":8924,"language":1367},[1365],"@ntfy host ntfy.example.com\nhandle @ntfy {\n    reverse_proxy localhost:8080\n}\n",[73,8926,8924],{"__ignoreMap":71},[15,8928,8929],{},"Два нюанса:",[30,8931,8932,8945],{},[33,8933,8934,8937,8938,5830,8941,8944],{},[478,8935,8936],{},"WebSocket"," — ntfy использует WebSocket для real-time подписок. Caddy проксирует его автоматически, но если вы за nginx, нужно явно прописать ",[73,8939,8940],{},"Upgrade",[73,8942,8943],{},"Connection"," заголовки.",[33,8946,8947,8950,8951,8953,8954,8957],{},[478,8948,8949],{},"Cloudflare SSL:Flexible"," — Cloudflare terminates SSL на своём edge, а до origin идёт plain HTTP. Поэтому Caddy слушает на ",[73,8952,8672],{},", а не ",[73,8955,8956],{},":443",". Если поставить SSL:Full, нужно ещё и сертификат на origin настраивать — для self-hosted хобби-проекта это overkill.",[22,8959,8961],{"id":8960},"пользователи-и-токены","Пользователи и токены",[15,8963,8964],{},"Создаём первого пользователя:",[66,8966,8968],{"className":68,"code":8967,"language":70,"meta":71,"style":71},"ntfy user add --role=admin admin\n",[73,8969,8970],{"__ignoreMap":71},[76,8971,8972,8974,8977,8980,8983],{"class":78,"line":79},[76,8973,8636],{"class":82},[76,8975,8976],{"class":86}," user",[76,8978,8979],{"class":86}," add",[76,8981,8982],{"class":161}," --role=admin",[76,8984,8985],{"class":86}," admin\n",[15,8987,8988,8989,2281],{},"Пароль запрашивается интерактивно. После этого все запросы требуют авторизации (из-за ",[73,8990,8991],{},"deny-all",[15,8993,8994],{},"Для программного доступа (скрипты, AI-агент) лучше использовать токены, а не логин\u002Fпароль:",[66,8996,8998],{"className":68,"code":8997,"language":70,"meta":71,"style":71},"ntfy token add admin\n",[73,8999,9000],{"__ignoreMap":71},[76,9001,9002,9004,9007,9009],{"class":78,"line":79},[76,9003,8636],{"class":82},[76,9005,9006],{"class":86}," token",[76,9008,8979],{"class":86},[76,9010,8985],{"class":86},[15,9012,9013,9014,9017,9018,1354],{},"Токен выглядит как ",[73,9015,9016],{},"tk_abc123...",". Передаётся в заголовке ",[73,9019,9020],{},"Authorization: Bearer tk_abc123...",[22,9022,9024],{"id":9023},"публикация-уведомлений","Публикация уведомлений",[15,9026,9027],{},"Отправить сообщение — один HTTP POST:",[66,9029,9031],{"className":68,"code":9030,"language":70,"meta":71,"style":71},"curl -u admin:password \\\n  -d \"Сервер перегрелся! CPU 95%\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[73,9032,9033,9045,9055],{"__ignoreMap":71},[76,9034,9035,9037,9040,9043],{"class":78,"line":79},[76,9036,403],{"class":82},[76,9038,9039],{"class":161}," -u",[76,9041,9042],{"class":86}," admin:password",[76,9044,169],{"class":168},[76,9046,9047,9050,9053],{"class":78,"line":118},[76,9048,9049],{"class":161},"  -d",[76,9051,9052],{"class":86}," \"Сервер перегрелся! CPU 95%\"",[76,9054,169],{"class":168},[76,9056,9057],{"class":78,"line":349},[76,9058,9059],{"class":86},"  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[15,9061,9062],{},"Или с токеном:",[66,9064,9066],{"className":68,"code":9065,"language":70,"meta":71,"style":71},"curl -H \"Authorization: Bearer tk_abc123...\" \\\n  -d \"Сервер перегрелся!\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[73,9067,9068,9080,9089],{"__ignoreMap":71},[76,9069,9070,9072,9075,9078],{"class":78,"line":79},[76,9071,403],{"class":82},[76,9073,9074],{"class":161}," -H",[76,9076,9077],{"class":86}," \"Authorization: Bearer tk_abc123...\"",[76,9079,169],{"class":168},[76,9081,9082,9084,9087],{"class":78,"line":118},[76,9083,9049],{"class":161},[76,9085,9086],{"class":86}," \"Сервер перегрелся!\"",[76,9088,169],{"class":168},[76,9090,9091],{"class":78,"line":349},[76,9092,9059],{"class":86},[15,9094,9095],{},"С заголовками для кастомизации:",[66,9097,9099],{"className":68,"code":9098,"language":70,"meta":71,"style":71},"curl -H \"Authorization: Bearer tk_abc123...\" \\\n  -H \"Title: Мониторинг\" \\\n  -H \"Priority: high\" \\\n  -H \"Tags: warning,fire\" \\\n  -d \"CPU 95%, RAM 90%\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[73,9100,9101,9111,9120,9129,9138,9147],{"__ignoreMap":71},[76,9102,9103,9105,9107,9109],{"class":78,"line":79},[76,9104,403],{"class":82},[76,9106,9074],{"class":161},[76,9108,9077],{"class":86},[76,9110,169],{"class":168},[76,9112,9113,9115,9118],{"class":78,"line":118},[76,9114,432],{"class":161},[76,9116,9117],{"class":86}," \"Title: Мониторинг\"",[76,9119,169],{"class":168},[76,9121,9122,9124,9127],{"class":78,"line":349},[76,9123,432],{"class":161},[76,9125,9126],{"class":86}," \"Priority: high\"",[76,9128,169],{"class":168},[76,9130,9131,9133,9136],{"class":78,"line":356},[76,9132,432],{"class":161},[76,9134,9135],{"class":86}," \"Tags: warning,fire\"",[76,9137,169],{"class":168},[76,9139,9140,9142,9145],{"class":78,"line":365},[76,9141,9049],{"class":161},[76,9143,9144],{"class":86}," \"CPU 95%, RAM 90%\"",[76,9146,169],{"class":168},[76,9148,9149],{"class":78,"line":370},[76,9150,9059],{"class":86},[22,9152,9154],{"id":9153},"подписка-на-уведомления","Подписка на уведомления",[15,9156,9157],{},"CLI:",[66,9159,9161],{"className":68,"code":9160,"language":70,"meta":71,"style":71},"ntfy subscribe ntfy.example.com\u002Fmonitoring\n",[73,9162,9163],{"__ignoreMap":71},[76,9164,9165,9167,9170],{"class":78,"line":79},[76,9166,8636],{"class":82},[76,9168,9169],{"class":86}," subscribe",[76,9171,9172],{"class":86}," ntfy.example.com\u002Fmonitoring\n",[15,9174,9175],{},"HTTP (long-polling):",[66,9177,9179],{"className":68,"code":9178,"language":70,"meta":71,"style":71},"curl -s ntfy.example.com\u002Fmonitoring\u002Fjson\n",[73,9180,9181],{"__ignoreMap":71},[76,9182,9183,9185,9187],{"class":78,"line":79},[76,9184,403],{"class":82},[76,9186,406],{"class":161},[76,9188,9189],{"class":86}," ntfy.example.com\u002Fmonitoring\u002Fjson\n",[15,9191,9192],{},"WebSocket (для интеграций):",[66,9194,9198],{"className":9195,"code":9196,"language":9197,"meta":71,"style":71},"language-javascript shiki shiki-themes github-light catppuccin-mocha","const ws = new WebSocket('wss:\u002F\u002Fntfy.example.com\u002Fmonitoring\u002Fws');\nws.onmessage = (e) => console.log(JSON.parse(e.data));\n","javascript",[73,9199,9200,9226],{"__ignoreMap":71},[76,9201,9202,9205,9208,9210,9214,9217,9219,9222,9224],{"class":78,"line":79},[76,9203,9204],{"class":103},"const",[76,9206,9207],{"class":2966}," ws",[76,9209,1567],{"class":111},[76,9211,9213],{"class":9212},"sf7P5"," new",[76,9215,9216],{"class":82}," WebSocket",[76,9218,1758],{"class":107},[76,9220,9221],{"class":86},"'wss:\u002F\u002Fntfy.example.com\u002Fmonitoring\u002Fws'",[76,9223,1796],{"class":107},[76,9225,2778],{"class":1549},[76,9227,9228,9231,9233,9236,9238,9241,9244,9246,9249,9252,9254,9257,9259,9262,9264,9267,9270,9272,9275],{"class":78,"line":118},[76,9229,9230],{"class":107},"ws",[76,9232,1354],{"class":8699},[76,9234,9235],{"class":82},"onmessage",[76,9237,1567],{"class":111},[76,9239,9240],{"class":1549}," (",[76,9242,9243],{"class":2267},"e",[76,9245,1796],{"class":1549},[76,9247,9248],{"class":103}," =>",[76,9250,9251],{"class":107}," console",[76,9253,1354],{"class":8699},[76,9255,9256],{"class":82},"log",[76,9258,1758],{"class":107},[76,9260,9261],{"class":189},"JSON",[76,9263,1354],{"class":8699},[76,9265,9266],{"class":82},"parse",[76,9268,9269],{"class":107},"(e",[76,9271,1354],{"class":8699},[76,9273,9274],{"class":107},"data))",[76,9276,2778],{"class":1549},[15,9278,9279,9280,9283],{},"Android\u002FiOS приложение — просто добавляете сервер ",[73,9281,9282],{},"https:\u002F\u002Fntfy.example.com"," и подписываетесь на топики.",[22,9285,9287],{"id":9286},"интеграция-с-hermes-webhooks","Интеграция с Hermes: webhooks",[15,9289,9290],{},"Самое интересное — подключить ntfy к AI-агенту. У Hermes есть webhook-система, и nfy поддерживает actions — автоматические HTTP-запросы при получении сообщения.",[15,9292,9293,9294,1550],{},"В ",[73,9295,8683],{},[66,9297,9299],{"className":8686,"code":9298,"language":8688,"meta":71,"style":71},"actions:\n  - action: \"webhook\"\n    label: \"Forward to Hermes\"\n    url: \"http:\u002F\u002Flocalhost:8644\u002Fwebhooks\u002Fntfy\"\n    headers:\n      Authorization: \"Bearer my-hermes-secret\"\n    body: |\n      {\n        \"topic\": \"{{ .Topic }}\",\n        \"message\": \"{{ .Message }}\",\n        \"title\": \"{{ .Title }}\",\n        \"sender\": \"{{ .Sender }}\",\n        \"time\": \"{{ .Time }}\"\n      }\n    topic: \"hermes\"\n",[73,9300,9301,9308,9321,9331,9341,9348,9358,9368,9373,9378,9383,9388,9393,9398,9403],{"__ignoreMap":71},[76,9302,9303,9306],{"class":78,"line":79},[76,9304,9305],{"class":8695},"actions",[76,9307,1593],{"class":8699},[76,9309,9310,9313,9316,9318],{"class":78,"line":118},[76,9311,9312],{"class":1549},"  -",[76,9314,9315],{"class":8695}," action",[76,9317,1550],{"class":8699},[76,9319,9320],{"class":86}," \"webhook\"\n",[76,9322,9323,9326,9328],{"class":78,"line":349},[76,9324,9325],{"class":8695},"    label",[76,9327,1550],{"class":8699},[76,9329,9330],{"class":86}," \"Forward to Hermes\"\n",[76,9332,9333,9336,9338],{"class":78,"line":356},[76,9334,9335],{"class":8695},"    url",[76,9337,1550],{"class":8699},[76,9339,9340],{"class":86}," \"http:\u002F\u002Flocalhost:8644\u002Fwebhooks\u002Fntfy\"\n",[76,9342,9343,9346],{"class":78,"line":365},[76,9344,9345],{"class":8695},"    headers",[76,9347,1593],{"class":8699},[76,9349,9350,9353,9355],{"class":78,"line":370},[76,9351,9352],{"class":8695},"      Authorization",[76,9354,1550],{"class":8699},[76,9356,9357],{"class":86}," \"Bearer my-hermes-secret\"\n",[76,9359,9360,9363,9365],{"class":78,"line":376},[76,9361,9362],{"class":8695},"    body",[76,9364,1550],{"class":8699},[76,9366,9367],{"class":103}," |\n",[76,9369,9370],{"class":78,"line":1628},[76,9371,9372],{"class":86},"      {\n",[76,9374,9375],{"class":78,"line":1636},[76,9376,9377],{"class":86},"        \"topic\": \"{{ .Topic }}\",\n",[76,9379,9380],{"class":78,"line":1645},[76,9381,9382],{"class":86},"        \"message\": \"{{ .Message }}\",\n",[76,9384,9385],{"class":78,"line":1656},[76,9386,9387],{"class":86},"        \"title\": \"{{ .Title }}\",\n",[76,9389,9390],{"class":78,"line":1668},[76,9391,9392],{"class":86},"        \"sender\": \"{{ .Sender }}\",\n",[76,9394,9395],{"class":78,"line":1678},[76,9396,9397],{"class":86},"        \"time\": \"{{ .Time }}\"\n",[76,9399,9400],{"class":78,"line":1686},[76,9401,9402],{"class":86},"      }\n",[76,9404,9405,9408,9410],{"class":78,"line":1691},[76,9406,9407],{"class":8695},"    topic",[76,9409,1550],{"class":8699},[76,9411,9412],{"class":86}," \"hermes\"\n",[15,9414,9415,9416,9418],{},"Теперь каждое сообщение в топик ",[73,9417,680],{}," автоматически пересылается в Hermes API. Агент может обработать уведомление и ответить.",[15,9420,9421,9424,9425,9427,9428,9431],{},[478,9422,9423],{},"Ловушка",": если Hermes отвечает в тот же топик ",[73,9426,680],{},", ntfy снова вызывает webhook, Hermes снова отвечает — бесконечный цикл. Решение: отвечать в другой топик (например, ",[73,9429,9430],{},"hermes-responses","), или фильтровать по sender\u002Fheaders в обработчике.",[22,9433,9435],{"id":9434},"реальные-кейсы","Реальные кейсы",[137,9437,9439],{"id":9438},"мониторинг-сервера","Мониторинг сервера",[15,9441,9442,9443,9446,9447,9450,9451,1354],{},"Cron-скрипт каждые 5 минут проверяет CPU, RAM, диск. Если значение порог — отправляем HTTP POST в ntfy. Приоритет ",[73,9444,9445],{},"high",", тег ",[73,9448,9449],{},"warning"," — и на телефоне сразу видно, что проблема. Скрипт занимает 5 строк bash, инициализация — один ",[73,9452,403],{},[137,9454,9456],{"id":9455},"алёрты-от-ai-агента","Алёрты от AI-агента",[15,9458,9459],{},"Hermes запускает cron-задачу (ежедневный брифинг), и результат шлёт в ntfy:",[66,9461,9464],{"className":9462,"code":9463,"language":1367},[1365],"hermes cron job → Hermes API → POST ntfy.example.com\u002Fhermes\n",[73,9465,9463],{"__ignoreMap":71},[15,9467,9468],{},"Пользователь видит уведомление на телефоне, открывает — там саммари новостей, задач, статус сервисов.",[137,9470,9472],{"id":9471},"уведомления-о-деплоях","Уведомления о деплоях",[15,9474,9475,9476,9479,9480,9483,9484,9486],{},"CI\u002FCD pipeline (или простой скрипт) шлёт статус деплоя: тег ",[73,9477,9478],{},"white_check_mark"," для успеха, ",[73,9481,9482],{},"x"," для провала, приоритет ",[73,9485,9445],{}," для критичных ошибок. Один POST-запрос — и вы видите результат на телефоне, не открывая CI.",[22,9488,9490],{"id":9489},"почему-не-telegram","Почему не Telegram",[15,9492,9493],{},"Telegram Bot API — отличный вариант, и я его тоже использую. Но у self-hosted ntfy есть преимущества:",[2550,9495,9496,9502,9508,9514,9520],{},[33,9497,9498,9501],{},[478,9499,9500],{},"Нет rate limit"," от Telegram (30 сообщений\u002Fсек на чат)",[33,9503,9504,9507],{},[478,9505,9506],{},"Нет зависимости"," от серверов Telegram (они иногда падают)",[33,9509,9510,9513],{},[478,9511,9512],{},"Полный контроль"," над данными (уведомления не проходят через Telegram)",[33,9515,9516,9519],{},[478,9517,9518],{},"Нет необходимости"," в Bot Token и chat ID — просто HTTP POST",[33,9521,9522,9525],{},[478,9523,9524],{},"WebSocket подписки"," встроены, без polling",[15,9527,9528],{},"Минусы: нет rich-контента (кнопки, inline keyboard), нет групповых чатов с историей, нет E2E шифрования. Для мониторинга и алертов — идеально. Для мессенджера — нет.",[22,9530,9532],{"id":9531},"безопасность-что-может-пойти-не-так","Безопасность: что может пойти не так",[137,9534,9536],{"id":9535},"публичные-топики","Публичные топики",[15,9538,9539,9540,9542,9543,9545,9546,9549],{},"Если ",[73,9541,8757],{}," не ",[73,9544,8991],{},", любой, кто угадает имя топика, может читать из него. Топик-имена — не секреты. ",[73,9547,9548],{},"ntfy subscribe ntfy.example.com\u002Fmy-secret-topic"," — это не защита, это security through obscurity.",[15,9551,9552,9553,9555],{},"Всегда ставьте ",[73,9554,8806],{}," и создавайте пользователей с явными правами на конкретные топики:",[66,9557,9559],{"className":68,"code":9558,"language":70,"meta":71,"style":71},"ntfy access admin monitoring rw\nntfy access admin hermes rw\nntfy access bot-hermes hermes rw\nntfy access bot-hermes monitoring ro\n",[73,9560,9561,9577,9590,9603],{"__ignoreMap":71},[76,9562,9563,9565,9568,9571,9574],{"class":78,"line":79},[76,9564,8636],{"class":82},[76,9566,9567],{"class":86}," access",[76,9569,9570],{"class":86}," admin",[76,9572,9573],{"class":86}," monitoring",[76,9575,9576],{"class":86}," rw\n",[76,9578,9579,9581,9583,9585,9588],{"class":78,"line":118},[76,9580,8636],{"class":82},[76,9582,9567],{"class":86},[76,9584,9570],{"class":86},[76,9586,9587],{"class":86}," hermes",[76,9589,9576],{"class":86},[76,9591,9592,9594,9596,9599,9601],{"class":78,"line":349},[76,9593,8636],{"class":82},[76,9595,9567],{"class":86},[76,9597,9598],{"class":86}," bot-hermes",[76,9600,9587],{"class":86},[76,9602,9576],{"class":86},[76,9604,9605,9607,9609,9611,9613],{"class":78,"line":356},[76,9606,8636],{"class":82},[76,9608,9567],{"class":86},[76,9610,9598],{"class":86},[76,9612,9573],{"class":86},[76,9614,9615],{"class":86}," ro\n",[137,9617,9619],{"id":9618},"rate-limiting","Rate limiting",[15,9621,9622],{},"ntfy имеет встроенный rate limiting (по умолчанию 250 сообщений в день на visitor). Для self-hosted с 1-2 пользователями — более чем достаточно. Но если вы шлете алерты каждые 10 секунд, можете упереться лимит.",[15,9624,9625,9626,1550],{},"Настройка в ",[73,9627,8683],{},[66,9629,9631],{"className":8686,"code":9630,"language":8688,"meta":71,"style":71},"visitor-subscription-limit: 30\nvisitor-request-limit-burst: 60\nvisitor-request-limit-replenish: 5s\n",[73,9632,9633,9643,9653],{"__ignoreMap":71},[76,9634,9635,9638,9640],{"class":78,"line":79},[76,9636,9637],{"class":8695},"visitor-subscription-limit",[76,9639,1550],{"class":8699},[76,9641,9642],{"class":189}," 30\n",[76,9644,9645,9648,9650],{"class":78,"line":118},[76,9646,9647],{"class":8695},"visitor-request-limit-burst",[76,9649,1550],{"class":8699},[76,9651,9652],{"class":189}," 60\n",[76,9654,9655,9658,9660],{"class":78,"line":349},[76,9656,9657],{"class":8695},"visitor-request-limit-replenish",[76,9659,1550],{"class":8699},[76,9661,9662],{"class":86}," 5s\n",[137,9664,9666],{"id":9665},"логирование","Логирование",[15,9668,9669],{},"ntfy пишет логи в stdout (systemd journal). Для продакшена стоит поднять логирование:",[66,9671,9673],{"className":8686,"code":9672,"language":8688,"meta":71,"style":71},"log-level: info\nlog-format: json\n",[73,9674,9675,9683],{"__ignoreMap":71},[76,9676,9677,9679,9681],{"class":78,"line":79},[76,9678,8767],{"class":8695},[76,9680,1550],{"class":8699},[76,9682,8772],{"class":86},[76,9684,9685,9688,9690],{"class":78,"line":118},[76,9686,9687],{"class":8695},"log-format",[76,9689,1550],{"class":8699},[76,9691,9692],{"class":86}," json\n",[15,9694,9695],{},"JSON-формат удобнее для парсинга в Loki\u002FELK, но для хобби-проекта достаточно text.",[22,9697,9699],{"id":9698},"миграция-и-бэкапы","Миграция и бэкапы",[15,9701,9702],{},"ntfy хранит всё в двух файлах:",[2550,9704,9705,9711],{},[33,9706,9707,9710],{},[73,9708,9709],{},"\u002Fvar\u002Fcache\u002Fntfy\u002Fcache.db"," — кеш сообщений (SQLite)",[33,9712,9713,9716],{},[73,9714,9715],{},"\u002Fvar\u002Flib\u002Fntfy\u002Fuser.db"," — пользователи и токены (SQLite)",[15,9718,9719],{},"Для бэкапа — просто копируйте эти файлы. Для миграции на другой сервер — перенесите файлы и конфиг.",[15,9721,9722,9724,9725,9727,9728,9731],{},[478,9723,8812],{},": при обновлении ntfy через apt, конфиг ",[73,9726,8683],{}," может быть перезаписан. Используйте ",[73,9729,9730],{},"dpkg --force-confold"," или бэкапите конфиг отдельно.",[22,9733,9735],{"id":9734},"траблшутинг","Траблшутинг",[137,9737,9739],{"id":9738},"ntfy-не-стартует-unable-to-open-database-file","ntfy не стартует: \"unable to open database file\"",[15,9741,9742],{},"Самая частая проблема. Проверьте:",[66,9744,9746],{"className":68,"code":9745,"language":70,"meta":71,"style":71},"ls -la \u002Fvar\u002Fcache\u002Fntfy \u002Fvar\u002Flib\u002Fntfy\n# Должны принадлежать _ntfy:_ntfy\n\njournalctl -u ntfy --no-pager -n 20\n# Смотрим последние логи\n",[73,9747,9748,9760,9765,9769,9788],{"__ignoreMap":71},[76,9749,9750,9753,9756,9758],{"class":78,"line":79},[76,9751,9752],{"class":82},"ls",[76,9754,9755],{"class":161}," -la",[76,9757,8888],{"class":86},[76,9759,8891],{"class":86},[76,9761,9762],{"class":78,"line":118},[76,9763,9764],{"class":352},"# Должны принадлежать _ntfy:_ntfy\n",[76,9766,9767],{"class":78,"line":349},[76,9768,346],{"emptyLinePlaceholder":345},[76,9770,9771,9774,9776,9779,9782,9785],{"class":78,"line":356},[76,9772,9773],{"class":82},"journalctl",[76,9775,9039],{"class":161},[76,9777,9778],{"class":86}," ntfy",[76,9780,9781],{"class":161}," --no-pager",[76,9783,9784],{"class":161}," -n",[76,9786,9787],{"class":189}," 20\n",[76,9789,9790],{"class":78,"line":365},[76,9791,9792],{"class":352},"# Смотрим последние логи\n",[137,9794,9796],{"id":9795},"уведомления-не-доходят-через-cloudflare","Уведомления не доходят через Cloudflare",[15,9798,9799,9800,9803,9804,1354],{},"Проверьте, что Cloudflare не кеширует API-ответы. ntfy API endpoints (",[73,9801,9802],{},"\u002Fv1\u002F...",") не должны кешироваться. В Dashboard: Caching → Configuration → Cache Level: Bypass для ",[73,9805,9806],{},"ntfy.example.com\u002Fv1\u002F*",[137,9808,9810],{"id":9809},"websocket-не-подключается","WebSocket не подключается",[15,9812,9813],{},"За Cloudflare WebSocket работает, но нужно убедиться, что:",[30,9815,9816,9821,9824],{},[33,9817,9818,9820],{},[73,9819,8794],{}," в конфиге ntfy",[33,9822,9823],{},"Cloudflare не блокирует WebSocket (на Free плане не блокирует)",[33,9825,9826],{},"Caddy\u002Fnginx проксирует Upgrade заголовки",[137,9828,9830],{"id":9829},"_403-forbidden-при-публикации","\"403 Forbidden\" при публикации",[15,9832,9833],{},"Пользователь не имеет доступа к топику. Проверьте:",[66,9835,9837],{"className":68,"code":9836,"language":70,"meta":71,"style":71},"ntfy access LIST  # Кто имеет доступ к чему\nntfy access admin my-topic rw  # Дать доступ\n",[73,9838,9839,9851],{"__ignoreMap":71},[76,9840,9841,9843,9845,9848],{"class":78,"line":79},[76,9842,8636],{"class":82},[76,9844,9567],{"class":86},[76,9846,9847],{"class":86}," LIST",[76,9849,9850],{"class":352},"  # Кто имеет доступ к чему\n",[76,9852,9853,9855,9857,9859,9862,9865],{"class":78,"line":118},[76,9854,8636],{"class":82},[76,9856,9567],{"class":86},[76,9858,9570],{"class":86},[76,9860,9861],{"class":86}," my-topic",[76,9863,9864],{"class":86}," rw",[76,9866,9867],{"class":352},"  # Дать доступ\n",[22,9869,9871],{"id":9870},"альтернативы","Альтернативы",[137,9873,9875],{"id":9874},"gotify","Gotify",[15,9877,9878],{},"Похож на ntfy, но написан на Go с другим API. Меньше комьюнити, меньше приложений. Если уже выбрали ntfy — нет смысла менять.",[137,9880,9882],{"id":9881},"apprise","Apprise",[15,9884,9885],{},"Python-библиотека для отправки уведомлений в 80+ сервисов (Telegram, Slack, Discord, ntfy, и т.д.). Хороша как unified sender, но не как сервер. Можно комбинировать: Apprise → ntfy → телефон.",[137,9887,9889],{"id":9888},"shoutrrr","Shoutrrr",[15,9891,9892],{},"Go-альтернатива Apprise. Тот же подход — единый интерфейс для разных notification backends.",[137,9894,9896],{"id":9895},"telegram-bot-api","Telegram Bot API",[15,9898,9899],{},"Самый очевидный вариант, и я его тоже использую. Но ntfy выигрывает для self-hosted: нет rate limit от Telegram, нет зависимости от серверов Telegram, полный контроль над данными. Telegram — для мессенджерного опыта. ntfy — для infrastructure alerts.",[22,9901,9903],{"id":9902},"ресурсы","Ресурсы",[15,9905,9906,9907,9910],{},"На типичном VPS ntfy потребляет ",[478,9908,9909],{},"~18 МБ RAM"," и практически не грузит CPU. За 4 часа работы — 51 сообщение опубликовано, 3 подписчика, 4 активных топика. Это капля в море.",[15,9912,9913],{},"Для сравнения: Prometheus + Alertmanager жрут ~200 МБ. Grafana — ещё ~150 МБ. ntfy с webhook-интеграцией покрывает 80% кейсов мониторинга для хобби-проекта без всей этой инфраструктуры.",[22,9915,2839],{"id":3398},[15,9917,9918,9919,9922],{},"ntfy — это быстро: от ",[73,9920,9921],{},"apt install"," до работающих push-уведомлений — минимум настроек. Go-бинарник, systemd-сервис, HTTP API. Ставите, настраиваете Caddy, создаёте пользователя — и у вас свой notification backend, который не зависит ни от кого.",[15,9924,9925],{},"Для self-hosted AI-агента — это must-have. Мониторинг, алерты, уведомления о задачах — всё через один простой протокол.",[652,9927,9928],{},"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 .soLUO, html code.shiki .soLUO{--shiki-light:#005CC5;--shiki-dark:#A6E3A1}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 .sEb-F, html code.shiki .sEb-F{--shiki-light:#22863A;--shiki-dark:#89B4FA}html pre.shiki code .sgPNX, html code.shiki .sgPNX{--shiki-light:#24292E;--shiki-dark:#94E2D5}html pre.shiki code .sNSVI, html code.shiki .sNSVI{--shiki-light:#005CC5;--shiki-dark:#FAB387}html pre.shiki code .saXKZ, html code.shiki .saXKZ{--shiki-light:#D73A49;--shiki-dark:#CBA6F7}html pre.shiki code .slTIY, html code.shiki .slTIY{--shiki-light:#24292E;--shiki-dark:#CDD6F4}html pre.shiki code .s_QEy, html code.shiki .s_QEy{--shiki-light:#24292E;--shiki-dark:#9399B2}html pre.shiki code .sPNDc, html code.shiki .sPNDc{--shiki-light:#24292E;--shiki-dark:#89B4FA}html pre.shiki code .s_VIv, html code.shiki .s_VIv{--shiki-light:#005CC5;--shiki-dark:#F5C2E7}html pre.shiki code .sbIxs, html code.shiki .sbIxs{--shiki-light:#005CC5;--shiki-dark:#CDD6F4}html pre.shiki code .s_Q3D, html code.shiki .s_Q3D{--shiki-light:#D73A49;--shiki-dark:#94E2D5}html pre.shiki code .sf7P5, html code.shiki .sf7P5{--shiki-light:#D73A49;--shiki-light-font-weight:inherit;--shiki-dark:#CBA6F7;--shiki-dark-font-weight:bold}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 .skkvY, html code.shiki .skkvY{--shiki-light:#6A737D;--shiki-light-font-style:inherit;--shiki-dark:#9399B2;--shiki-dark-font-style:italic}",{"title":71,"searchDepth":118,"depth":118,"links":9930},[9931,9932,9933,9934,9935,9936,9937,9938,9939,9944,9945,9950,9951,9957,9963,9964],{"id":8640,"depth":118,"text":8641},{"id":8676,"depth":118,"text":8677},{"id":8859,"depth":118,"text":8860},{"id":8913,"depth":118,"text":8914},{"id":8960,"depth":118,"text":8961},{"id":9023,"depth":118,"text":9024},{"id":9153,"depth":118,"text":9154},{"id":9286,"depth":118,"text":9287},{"id":9434,"depth":118,"text":9435,"children":9940},[9941,9942,9943],{"id":9438,"depth":349,"text":9439},{"id":9455,"depth":349,"text":9456},{"id":9471,"depth":349,"text":9472},{"id":9489,"depth":118,"text":9490},{"id":9531,"depth":118,"text":9532,"children":9946},[9947,9948,9949],{"id":9535,"depth":349,"text":9536},{"id":9618,"depth":349,"text":9619},{"id":9665,"depth":349,"text":9666},{"id":9698,"depth":118,"text":9699},{"id":9734,"depth":118,"text":9735,"children":9952},[9953,9954,9955,9956],{"id":9738,"depth":349,"text":9739},{"id":9795,"depth":349,"text":9796},{"id":9809,"depth":349,"text":9810},{"id":9829,"depth":349,"text":9830},{"id":9870,"depth":118,"text":9871,"children":9958},[9959,9960,9961,9962],{"id":9874,"depth":349,"text":9875},{"id":9881,"depth":349,"text":9882},{"id":9888,"depth":349,"text":9889},{"id":9895,"depth":349,"text":9896},{"id":9902,"depth":118,"text":9903},{"id":3398,"depth":118,"text":2839},"Как поднять свой ntfy-сервер для push-уведомлений: Caddy reverse proxy, токены доступа, интеграция с AI-агентом через webhooks. Мониторинг, алерты и ловушка с бесконечным циклом.",{},"\u002Fblog\u002Fself-hosted-ntfy-notifications",{"title":8620,"description":9965},"blog\u002Fself-hosted-ntfy-notifications",[8636,3465,9971,9972,9973,680],"notifications","vps","caddy","nVgsVh3Yj-JM6u9Vp0nbm4h2SG5rxy-kOsA8I1D8rJc",{"id":9976,"title":9977,"body":9978,"date":671,"description":11153,"extension":673,"meta":11154,"navigation":345,"path":11155,"readingTime":676,"seo":11156,"stem":11157,"tags":11158,"__hash__":11159},"articles\u002Fblog\u002Fself-hosted-ntfy-notifications.en.md","Self-Hosted ntfy: Your Own Push Notification Server",{"type":8,"value":9979,"toc":11117},[9980,9983,9986,9993,9997,10000,10014,10023,10027,10032,10100,10103,10113,10123,10130,10139,10171,10175,10185,10209,10215,10218,10224,10229,10232,10255,10259,10262,10278,10283,10286,10300,10306,10310,10313,10341,10344,10373,10376,10441,10445,10447,10459,10461,10473,10476,10540,10546,10550,10553,10558,10652,10658,10670,10674,10678,10689,10693,10696,10701,10704,10708,10720,10724,10727,10759,10762,10766,10770,10782,10788,10840,10843,10846,10851,10879,10883,10886,10906,10909,10913,10916,10928,10931,10942,10946,10950,10953,10996,11000,11008,11012,11015,11028,11032,11035,11066,11070,11072,11075,11077,11080,11082,11085,11087,11090,11094,11101,11104,11106,11112,11115],[11,9981,9977],{"id":9982},"self-hosted-ntfy-your-own-push-notification-server",[15,9984,9985],{},"I run an AI agent (Hermes), monitoring tools, a blog, and a bunch of small services on my server. I needed to receive notifications from all this infrastructure on my phone — not via the Telegram Bot API (with its rate limits and dependency on Telegram's servers), but through my own channel that I control.",[15,9987,9988,9989,9992],{},"The solution is ",[58,9990,8636],{"href":8634,"rel":9991},[62],": a lightweight Go server, HTTP pub-sub, Android\u002FiOS apps, and zero vendor lock-in. Here's how I set it up and what I learned.",[22,9994,9996],{"id":9995},"installation","Installation",[15,9998,9999],{},"On Debian\u002FUbuntu, it's straightforward:",[66,10001,10002],{"className":68,"code":8647,"language":70,"meta":71,"style":71},[73,10003,10004],{"__ignoreMap":71},[76,10005,10006,10008,10010,10012],{"class":78,"line":79},[76,10007,8654],{"class":82},[76,10009,87],{"class":86},[76,10011,8659],{"class":161},[76,10013,8662],{"class":86},[15,10015,10016,10017,10019,10020,10022],{},"The package creates an ",[73,10018,8668],{}," user, a systemd service, and a default config. But the \"default\" listens on ",[73,10021,8672],{}," without authentication. For production, this needs to change.",[22,10024,10026],{"id":10025},"configuration","Configuration",[15,10028,10029,10030,1550],{},"File ",[73,10031,8683],{},[66,10033,10034],{"className":8686,"code":8687,"language":8688,"meta":71,"style":71},[73,10035,10036,10044,10052,10060,10068,10076,10084,10092],{"__ignoreMap":71},[76,10037,10038,10040,10042],{"class":78,"line":79},[76,10039,8696],{"class":8695},[76,10041,1550],{"class":8699},[76,10043,8702],{"class":86},[76,10045,10046,10048,10050],{"class":78,"line":118},[76,10047,8707],{"class":8695},[76,10049,1550],{"class":8699},[76,10051,8712],{"class":86},[76,10053,10054,10056,10058],{"class":78,"line":349},[76,10055,8717],{"class":8695},[76,10057,1550],{"class":8699},[76,10059,8722],{"class":189},[76,10061,10062,10064,10066],{"class":78,"line":356},[76,10063,8727],{"class":8695},[76,10065,1550],{"class":8699},[76,10067,8732],{"class":86},[76,10069,10070,10072,10074],{"class":78,"line":365},[76,10071,8737],{"class":8695},[76,10073,1550],{"class":8699},[76,10075,8742],{"class":86},[76,10077,10078,10080,10082],{"class":78,"line":370},[76,10079,8747],{"class":8695},[76,10081,1550],{"class":8699},[76,10083,8752],{"class":86},[76,10085,10086,10088,10090],{"class":78,"line":376},[76,10087,8757],{"class":8695},[76,10089,1550],{"class":8699},[76,10091,8762],{"class":86},[76,10093,10094,10096,10098],{"class":78,"line":1628},[76,10095,8767],{"class":8695},[76,10097,1550],{"class":8699},[76,10099,8772],{"class":86},[15,10101,10102],{},"Let's break down the key points:",[15,10104,10105,10109,10110,10112],{},[478,10106,10107],{},[73,10108,8782],{}," — listen only on localhost. All external traffic goes through the reverse proxy. If you set ",[73,10111,8786],{},", ntfy will be accessible directly, bypassing SSL and rate limiting.",[15,10114,10115,10119,10120,10122],{},[478,10116,10117],{},[73,10118,8794],{}," — mandatory if you're behind Caddy\u002Fnginx. Without this, ntfy won't see subscribers' real IP addresses (it will see ",[73,10121,8798],{},"), and rate limiting won't work correctly.",[15,10124,10125,10129],{},[478,10126,10127],{},[73,10128,8806],{}," — everything is closed by default. Without this, anyone who guesses a topic name can read from and write to it. This is fine for the public ntfy.sh, but not for self-hosted.",[15,10131,10132,10135,10136,10138],{},[478,10133,10134],{},"Pitfall",": Don't write the config via ",[73,10137,8816],{}," in the terminal — some security scanners block heredocs in bash. Use Python instead:",[66,10140,10141],{"className":1527,"code":8820,"language":1529,"meta":71,"style":71},[73,10142,10143,10149],{"__ignoreMap":71},[76,10144,10145,10147],{"class":78,"line":79},[76,10146,1740],{"class":103},[76,10148,8829],{"class":107},[76,10150,10151,10153,10155,10157,10159,10161,10163,10165,10167,10169],{"class":78,"line":118},[76,10152,8834],{"class":107},[76,10154,1354],{"class":1549},[76,10156,8839],{"class":1754},[76,10158,1758],{"class":1549},[76,10160,8844],{"class":86},[76,10162,2281],{"class":1549},[76,10164,8849],{"class":1754},[76,10166,1758],{"class":1549},[76,10168,8854],{"class":107},[76,10170,1764],{"class":1549},[22,10172,10174],{"id":10173},"directory-permissions","Directory Permissions",[15,10176,10177,10178,10180,10181,7497,10183,1550],{},"The packaged ",[73,10179,8668],{}," user must have write permissions to ",[73,10182,8869],{},[73,10184,8872],{},[66,10186,10187],{"className":68,"code":8875,"language":70,"meta":71,"style":71},[73,10188,10189,10199],{"__ignoreMap":71},[76,10190,10191,10193,10195,10197],{"class":78,"line":79},[76,10192,8882],{"class":82},[76,10194,8885],{"class":161},[76,10196,8888],{"class":86},[76,10198,8891],{"class":86},[76,10200,10201,10203,10205,10207],{"class":78,"line":118},[76,10202,8896],{"class":82},[76,10204,8899],{"class":86},[76,10206,8888],{"class":86},[76,10208,8891],{"class":86},[15,10210,10211,10212,10214],{},"Without this, the service will crash with ",[73,10213,8909],{},". Classic.",[22,10216,10217],{"id":8913},"Caddy Reverse Proxy",[15,10219,10220,10221,10223],{},"I have Caddy listening on ",[73,10222,8672],{}," (Cloudflare handles SSL termination externally). Config for ntfy:",[66,10225,10227],{"className":10226,"code":8924,"language":1367},[1365],[73,10228,8924],{"__ignoreMap":71},[15,10230,10231],{},"Two nuances:",[30,10233,10234,10244],{},[33,10235,10236,10238,10239,7497,10241,10243],{},[478,10237,8936],{}," — ntfy uses WebSocket for real-time subscriptions. Caddy proxies it automatically, but if you're behind nginx, you need to explicitly configure the ",[73,10240,8940],{},[73,10242,8943],{}," headers.",[33,10245,10246,10248,10249,10251,10252,10254],{},[478,10247,8949],{}," — Cloudflare terminates SSL at its edge, and plain HTTP goes to the origin. That's why Caddy listens on ",[73,10250,8672],{},", not ",[73,10253,8956],{},". If you switch to SSL:Full, you'll also need to configure a certificate on the origin — overkill for a self-hosted hobby project.",[22,10256,10258],{"id":10257},"users-and-tokens","Users and Tokens",[15,10260,10261],{},"Create the first user:",[66,10263,10264],{"className":68,"code":8967,"language":70,"meta":71,"style":71},[73,10265,10266],{"__ignoreMap":71},[76,10267,10268,10270,10272,10274,10276],{"class":78,"line":79},[76,10269,8636],{"class":82},[76,10271,8976],{"class":86},[76,10273,8979],{"class":86},[76,10275,8982],{"class":161},[76,10277,8985],{"class":86},[15,10279,10280,10281,2281],{},"The password is requested interactively. After this, all requests require authentication (due to ",[73,10282,8991],{},[15,10284,10285],{},"For programmatic access (scripts, AI agent), it's better to use tokens instead of login\u002Fpassword:",[66,10287,10288],{"className":68,"code":8997,"language":70,"meta":71,"style":71},[73,10289,10290],{"__ignoreMap":71},[76,10291,10292,10294,10296,10298],{"class":78,"line":79},[76,10293,8636],{"class":82},[76,10295,9006],{"class":86},[76,10297,8979],{"class":86},[76,10299,8985],{"class":86},[15,10301,10302,10303,10305],{},"The token looks like ",[73,10304,9016],{},". It is passed in the header `Authorization: Bearer ***",[22,10307,10309],{"id":10308},"publishing-notifications","Publishing Notifications",[15,10311,10312],{},"Sending a message is a single HTTP POST:",[66,10314,10316],{"className":68,"code":10315,"language":70,"meta":71,"style":71},"curl -u admin:password \\\n  -d \"Server overheated! CPU 95%\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[73,10317,10318,10328,10337],{"__ignoreMap":71},[76,10319,10320,10322,10324,10326],{"class":78,"line":79},[76,10321,403],{"class":82},[76,10323,9039],{"class":161},[76,10325,9042],{"class":86},[76,10327,169],{"class":168},[76,10329,10330,10332,10335],{"class":78,"line":118},[76,10331,9049],{"class":161},[76,10333,10334],{"class":86}," \"Server overheated! CPU 95%\"",[76,10336,169],{"class":168},[76,10338,10339],{"class":78,"line":349},[76,10340,9059],{"class":86},[15,10342,10343],{},"Or with a token:",[66,10345,10347],{"className":68,"code":10346,"language":70,"meta":71,"style":71},"curl -H \"Authorization: Bearer *** \\\n  -d \"Server overheated!\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[73,10348,10349,10359,10369],{"__ignoreMap":71},[76,10350,10351,10353,10355,10357],{"class":78,"line":79},[76,10352,403],{"class":82},[76,10354,9074],{"class":161},[76,10356,1026],{"class":86},[76,10358,1029],{"class":168},[76,10360,10361,10364,10367],{"class":78,"line":118},[76,10362,10363],{"class":86},"  -d \"Server",[76,10365,10366],{"class":86}," overheated!\" ",[76,10368,1029],{"class":168},[76,10370,10371],{"class":78,"line":349},[76,10372,9059],{"class":86},[15,10374,10375],{},"With headers for customization:",[66,10377,10379],{"className":68,"code":10378,"language":70,"meta":71,"style":71},"curl -H \"Authorization: Bearer *** \\\n  -H \"Title: Monitoring\" \\\n  -H \"Priority: high\" \\\n  -H \"Tags: warning,fire\" \\\n  -d \"CPU 95%, RAM 90%\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[73,10380,10381,10391,10401,10411,10421,10437],{"__ignoreMap":71},[76,10382,10383,10385,10387,10389],{"class":78,"line":79},[76,10384,403],{"class":82},[76,10386,9074],{"class":161},[76,10388,1026],{"class":86},[76,10390,1029],{"class":168},[76,10392,10393,10396,10399],{"class":78,"line":118},[76,10394,10395],{"class":86},"  -H \"Title:",[76,10397,10398],{"class":86}," Monitoring\" ",[76,10400,1029],{"class":168},[76,10402,10403,10406,10409],{"class":78,"line":349},[76,10404,10405],{"class":86},"  -H \"Priority:",[76,10407,10408],{"class":86}," high\" ",[76,10410,1029],{"class":168},[76,10412,10413,10416,10419],{"class":78,"line":356},[76,10414,10415],{"class":86},"  -H \"Tags:",[76,10417,10418],{"class":86}," warning,fire\" ",[76,10420,1029],{"class":168},[76,10422,10423,10426,10429,10432,10435],{"class":78,"line":365},[76,10424,10425],{"class":86},"  -d \"CPU",[76,10427,10428],{"class":86}," 95%,",[76,10430,10431],{"class":86}," RAM",[76,10433,10434],{"class":86}," 90%\" ",[76,10436,1029],{"class":168},[76,10438,10439],{"class":78,"line":370},[76,10440,9059],{"class":86},[22,10442,10444],{"id":10443},"subscribing-to-notifications","Subscribing to Notifications",[15,10446,9157],{},[66,10448,10449],{"className":68,"code":9160,"language":70,"meta":71,"style":71},[73,10450,10451],{"__ignoreMap":71},[76,10452,10453,10455,10457],{"class":78,"line":79},[76,10454,8636],{"class":82},[76,10456,9169],{"class":86},[76,10458,9172],{"class":86},[15,10460,9175],{},[66,10462,10463],{"className":68,"code":9178,"language":70,"meta":71,"style":71},[73,10464,10465],{"__ignoreMap":71},[76,10466,10467,10469,10471],{"class":78,"line":79},[76,10468,403],{"class":82},[76,10470,406],{"class":161},[76,10472,9189],{"class":86},[15,10474,10475],{},"WebSocket (for integrations):",[66,10477,10478],{"className":9195,"code":9196,"language":9197,"meta":71,"style":71},[73,10479,10480,10500],{"__ignoreMap":71},[76,10481,10482,10484,10486,10488,10490,10492,10494,10496,10498],{"class":78,"line":79},[76,10483,9204],{"class":103},[76,10485,9207],{"class":2966},[76,10487,1567],{"class":111},[76,10489,9213],{"class":9212},[76,10491,9216],{"class":82},[76,10493,1758],{"class":107},[76,10495,9221],{"class":86},[76,10497,1796],{"class":107},[76,10499,2778],{"class":1549},[76,10501,10502,10504,10506,10508,10510,10512,10514,10516,10518,10520,10522,10524,10526,10528,10530,10532,10534,10536,10538],{"class":78,"line":118},[76,10503,9230],{"class":107},[76,10505,1354],{"class":8699},[76,10507,9235],{"class":82},[76,10509,1567],{"class":111},[76,10511,9240],{"class":1549},[76,10513,9243],{"class":2267},[76,10515,1796],{"class":1549},[76,10517,9248],{"class":103},[76,10519,9251],{"class":107},[76,10521,1354],{"class":8699},[76,10523,9256],{"class":82},[76,10525,1758],{"class":107},[76,10527,9261],{"class":189},[76,10529,1354],{"class":8699},[76,10531,9266],{"class":82},[76,10533,9269],{"class":107},[76,10535,1354],{"class":8699},[76,10537,9274],{"class":107},[76,10539,2778],{"class":1549},[15,10541,10542,10543,10545],{},"Android\u002FiOS app — just add the server ",[73,10544,9282],{}," and subscribe to topics.",[22,10547,10549],{"id":10548},"integration-with-hermes-webhooks","Integration with Hermes: Webhooks",[15,10551,10552],{},"The most interesting part is connecting ntfy to an AI agent. Hermes has a webhook system, and ntfy supports actions — automatic HTTP requests triggered when a message is received.",[15,10554,10555,10556,1550],{},"In ",[73,10557,8683],{},[66,10559,10560],{"className":8686,"code":9298,"language":8688,"meta":71,"style":71},[73,10561,10562,10568,10578,10586,10594,10600,10608,10616,10620,10624,10628,10632,10636,10640,10644],{"__ignoreMap":71},[76,10563,10564,10566],{"class":78,"line":79},[76,10565,9305],{"class":8695},[76,10567,1593],{"class":8699},[76,10569,10570,10572,10574,10576],{"class":78,"line":118},[76,10571,9312],{"class":1549},[76,10573,9315],{"class":8695},[76,10575,1550],{"class":8699},[76,10577,9320],{"class":86},[76,10579,10580,10582,10584],{"class":78,"line":349},[76,10581,9325],{"class":8695},[76,10583,1550],{"class":8699},[76,10585,9330],{"class":86},[76,10587,10588,10590,10592],{"class":78,"line":356},[76,10589,9335],{"class":8695},[76,10591,1550],{"class":8699},[76,10593,9340],{"class":86},[76,10595,10596,10598],{"class":78,"line":365},[76,10597,9345],{"class":8695},[76,10599,1593],{"class":8699},[76,10601,10602,10604,10606],{"class":78,"line":370},[76,10603,9352],{"class":8695},[76,10605,1550],{"class":8699},[76,10607,9357],{"class":86},[76,10609,10610,10612,10614],{"class":78,"line":376},[76,10611,9362],{"class":8695},[76,10613,1550],{"class":8699},[76,10615,9367],{"class":103},[76,10617,10618],{"class":78,"line":1628},[76,10619,9372],{"class":86},[76,10621,10622],{"class":78,"line":1636},[76,10623,9377],{"class":86},[76,10625,10626],{"class":78,"line":1645},[76,10627,9382],{"class":86},[76,10629,10630],{"class":78,"line":1656},[76,10631,9387],{"class":86},[76,10633,10634],{"class":78,"line":1668},[76,10635,9392],{"class":86},[76,10637,10638],{"class":78,"line":1678},[76,10639,9397],{"class":86},[76,10641,10642],{"class":78,"line":1686},[76,10643,9402],{"class":86},[76,10645,10646,10648,10650],{"class":78,"line":1691},[76,10647,9407],{"class":8695},[76,10649,1550],{"class":8699},[76,10651,9412],{"class":86},[15,10653,10654,10655,10657],{},"Now every message in the ",[73,10656,680],{}," topic is automatically forwarded to the Hermes API. The agent can process the notification and respond.",[15,10659,10660,10663,10664,10666,10667,10669],{},[478,10661,10662],{},"Trap",": If Hermes replies to the same ",[73,10665,680],{}," topic, ntfy triggers the webhook again, Hermes replies again — an infinite loop. Solution: reply to a different topic (e.g., ",[73,10668,9430],{},"), or filter by sender\u002Fheaders in the handler.",[22,10671,10673],{"id":10672},"real-world-use-cases","Real-World Use Cases",[137,10675,10677],{"id":10676},"server-monitoring","Server Monitoring",[15,10679,10680,10681,10683,10684,10686,10687,1354],{},"A cron script checks CPU, RAM, and disk every 5 minutes. If a threshold is exceeded, it sends an HTTP POST to ntfy. Priority ",[73,10682,9445],{},", tag ",[73,10685,9449],{}," — and you instantly see the problem on your phone. The script is 5 lines of bash, initialization is a single ",[73,10688,403],{},[137,10690,10692],{"id":10691},"alerts-from-an-ai-agent","Alerts from an AI Agent",[15,10694,10695],{},"Hermes runs a cron job (daily briefing) and sends the result to ntfy:",[66,10697,10699],{"className":10698,"code":9463,"language":1367},[1365],[73,10700,9463],{"__ignoreMap":71},[15,10702,10703],{},"The user sees a notification on their phone, opens it, and finds a summary of news, tasks, and service statuses.",[137,10705,10707],{"id":10706},"deployment-notifications","Deployment Notifications",[15,10709,10710,10711,10713,10714,10716,10717,10719],{},"A CI\u002FCD pipeline (or simple script) sends deployment status: tag ",[73,10712,9478],{}," for success, ",[73,10715,9482],{}," for failure, priority ",[73,10718,9445],{}," for critical errors. One POST request — and you see the result on your phone without opening the CI dashboard.",[22,10721,10723],{"id":10722},"why-not-telegram","Why Not Telegram",[15,10725,10726],{},"Telegram Bot API is a great option, and I use it too. But self-hosted ntfy has advantages:",[2550,10728,10729,10735,10741,10747,10753],{},[33,10730,10731,10734],{},[478,10732,10733],{},"No rate limits"," from Telegram (30 messages\u002Fsec per chat)",[33,10736,10737,10740],{},[478,10738,10739],{},"No dependency"," on Telegram servers (they go down sometimes)",[33,10742,10743,10746],{},[478,10744,10745],{},"Full control"," over data (notifications don't pass through Telegram)",[33,10748,10749,10752],{},[478,10750,10751],{},"No need"," for a Bot Token and chat ID — just an HTTP POST",[33,10754,10755,10758],{},[478,10756,10757],{},"WebSocket subscriptions"," built-in, no polling required",[15,10760,10761],{},"Downsides: no rich content (buttons, inline keyboards), no group chats with history, no E2E encryption. For monitoring and alerts — perfect. As a messenger — no.",[22,10763,10765],{"id":10764},"security-what-could-go-wrong","Security: What Could Go Wrong",[137,10767,10769],{"id":10768},"public-topics","Public Topics",[15,10771,10772,10773,10775,10776,10778,10779,10781],{},"If ",[73,10774,8757],{}," isn't ",[73,10777,8991],{},", anyone who guesses a topic name can read from it. Topic names are not secrets. ",[73,10780,9548],{}," is not protection; it's security through obscurity.",[15,10783,10784,10785,10787],{},"Always set ",[73,10786,8806],{}," and create users with explicit permissions for specific topics:",[66,10789,10790],{"className":68,"code":9558,"language":70,"meta":71,"style":71},[73,10791,10792,10804,10816,10828],{"__ignoreMap":71},[76,10793,10794,10796,10798,10800,10802],{"class":78,"line":79},[76,10795,8636],{"class":82},[76,10797,9567],{"class":86},[76,10799,9570],{"class":86},[76,10801,9573],{"class":86},[76,10803,9576],{"class":86},[76,10805,10806,10808,10810,10812,10814],{"class":78,"line":118},[76,10807,8636],{"class":82},[76,10809,9567],{"class":86},[76,10811,9570],{"class":86},[76,10813,9587],{"class":86},[76,10815,9576],{"class":86},[76,10817,10818,10820,10822,10824,10826],{"class":78,"line":349},[76,10819,8636],{"class":82},[76,10821,9567],{"class":86},[76,10823,9598],{"class":86},[76,10825,9587],{"class":86},[76,10827,9576],{"class":86},[76,10829,10830,10832,10834,10836,10838],{"class":78,"line":356},[76,10831,8636],{"class":82},[76,10833,9567],{"class":86},[76,10835,9598],{"class":86},[76,10837,9573],{"class":86},[76,10839,9615],{"class":86},[137,10841,10842],{"id":9618},"Rate Limiting",[15,10844,10845],{},"ntfy has built-in rate limiting (default 250 messages per day per visitor). For self-hosted setups with 1-2 users, this is more than enough. But if you send alerts every 10 seconds, you might hit the limit.",[15,10847,10848,10849,1550],{},"Configuration in ",[73,10850,8683],{},[66,10852,10853],{"className":8686,"code":9630,"language":8688,"meta":71,"style":71},[73,10854,10855,10863,10871],{"__ignoreMap":71},[76,10856,10857,10859,10861],{"class":78,"line":79},[76,10858,9637],{"class":8695},[76,10860,1550],{"class":8699},[76,10862,9642],{"class":189},[76,10864,10865,10867,10869],{"class":78,"line":118},[76,10866,9647],{"class":8695},[76,10868,1550],{"class":8699},[76,10870,9652],{"class":189},[76,10872,10873,10875,10877],{"class":78,"line":349},[76,10874,9657],{"class":8695},[76,10876,1550],{"class":8699},[76,10878,9662],{"class":86},[137,10880,10882],{"id":10881},"logging","Logging",[15,10884,10885],{},"ntfy writes logs to stdout (systemd journal). For production, it's worth configuring logging:",[66,10887,10888],{"className":8686,"code":9672,"language":8688,"meta":71,"style":71},[73,10889,10890,10898],{"__ignoreMap":71},[76,10891,10892,10894,10896],{"class":78,"line":79},[76,10893,8767],{"class":8695},[76,10895,1550],{"class":8699},[76,10897,8772],{"class":86},[76,10899,10900,10902,10904],{"class":78,"line":118},[76,10901,9687],{"class":8695},[76,10903,1550],{"class":8699},[76,10905,9692],{"class":86},[15,10907,10908],{},"JSON format is easier to parse in Loki\u002FELK, but text is sufficient for a hobby project.",[22,10910,10912],{"id":10911},"migration-and-backups","Migration and Backups",[15,10914,10915],{},"ntfy stores everything in two files:",[2550,10917,10918,10923],{},[33,10919,10920,10922],{},[73,10921,9709],{}," — message cache (SQLite)",[33,10924,10925,10927],{},[73,10926,9715],{}," — users and tokens (SQLite)",[15,10929,10930],{},"For backups, simply copy these files. To migrate to another server, transfer the files and config.",[15,10932,10933,10935,10936,10938,10939,10941],{},[478,10934,10134],{},": When updating ntfy via apt, the config ",[73,10937,8683],{}," may be overwritten. Use ",[73,10940,9730],{}," or back up the config separately.",[22,10943,10945],{"id":10944},"troubleshooting","Troubleshooting",[137,10947,10949],{"id":10948},"ntfy-wont-start-unable-to-open-database-file","ntfy won't start: \"unable to open database file\"",[15,10951,10952],{},"The most common issue. Check:",[66,10954,10956],{"className":68,"code":10955,"language":70,"meta":71,"style":71},"ls -la \u002Fvar\u002Fcache\u002Fntfy \u002Fvar\u002Flib\u002Fntfy\n# Should be owned by _ntfy:_ntfy\n\njournalctl -u ntfy --no-pager -n 20\n# Check recent logs\n",[73,10957,10958,10968,10973,10977,10991],{"__ignoreMap":71},[76,10959,10960,10962,10964,10966],{"class":78,"line":79},[76,10961,9752],{"class":82},[76,10963,9755],{"class":161},[76,10965,8888],{"class":86},[76,10967,8891],{"class":86},[76,10969,10970],{"class":78,"line":118},[76,10971,10972],{"class":352},"# Should be owned by _ntfy:_ntfy\n",[76,10974,10975],{"class":78,"line":349},[76,10976,346],{"emptyLinePlaceholder":345},[76,10978,10979,10981,10983,10985,10987,10989],{"class":78,"line":356},[76,10980,9773],{"class":82},[76,10982,9039],{"class":161},[76,10984,9778],{"class":86},[76,10986,9781],{"class":161},[76,10988,9784],{"class":161},[76,10990,9787],{"class":189},[76,10992,10993],{"class":78,"line":365},[76,10994,10995],{"class":352},"# Check recent logs\n",[137,10997,10999],{"id":10998},"notifications-arent-arriving-via-cloudflare","Notifications aren't arriving via Cloudflare",[15,11001,11002,11003,11005,11006,1354],{},"Ensure Cloudflare isn't caching API responses. ntfy API endpoints (",[73,11004,9802],{},") should not be cached. In the Dashboard: Caching → Configuration → Cache Level: Bypass for ",[73,11007,9806],{},[137,11009,11011],{"id":11010},"websocket-wont-connect","WebSocket won't connect",[15,11013,11014],{},"Behind Cloudflare, WebSocket works, but ensure that:",[30,11016,11017,11022,11025],{},[33,11018,11019,11021],{},[73,11020,8794],{}," is set in the ntfy config",[33,11023,11024],{},"Cloudflare isn't blocking WebSocket (Free plan doesn't block it)",[33,11026,11027],{},"Caddy\u002Fnginx proxies Upgrade headers",[137,11029,11031],{"id":11030},"_403-forbidden-when-publishing","\"403 Forbidden\" when publishing",[15,11033,11034],{},"The user lacks access to the topic. Check:",[66,11036,11038],{"className":68,"code":11037,"language":70,"meta":71,"style":71},"ntfy access LIST  # Who has access to what\nntfy access admin my-topic rw  # Grant access\n",[73,11039,11040,11051],{"__ignoreMap":71},[76,11041,11042,11044,11046,11048],{"class":78,"line":79},[76,11043,8636],{"class":82},[76,11045,9567],{"class":86},[76,11047,9847],{"class":86},[76,11049,11050],{"class":352},"  # Who has access to what\n",[76,11052,11053,11055,11057,11059,11061,11063],{"class":78,"line":118},[76,11054,8636],{"class":82},[76,11056,9567],{"class":86},[76,11058,9570],{"class":86},[76,11060,9861],{"class":86},[76,11062,9864],{"class":86},[76,11064,11065],{"class":352},"  # Grant access\n",[22,11067,11069],{"id":11068},"alternatives","Alternatives",[137,11071,9875],{"id":9874},[15,11073,11074],{},"Similar to ntfy, but written in Go with a different API. Smaller community, fewer apps. If you've already chosen ntfy, there's no reason to switch.",[137,11076,9882],{"id":9881},[15,11078,11079],{},"A Python library for sending notifications to 80+ services (Telegram, Slack, Discord, ntfy, etc.). Great as a unified sender, but not as a server. Can be combined: Apprise → ntfy → phone.",[137,11081,9889],{"id":9888},[15,11083,11084],{},"A Go alternative to Apprise. Same approach — a unified interface for various notification backends.",[137,11086,9896],{"id":9895},[15,11088,11089],{},"The most obvious option, and I use it too. But ntfy wins for self-hosted setups: no Telegram rate limits, no dependency on Telegram servers, full data control. Telegram is for the messenger experience. ntfy is for infrastructure alerts.",[22,11091,11093],{"id":11092},"resources","Resources",[15,11095,11096,11097,11100],{},"On a typical VPS, ntfy consumes ",[478,11098,11099],{},"~18 MB of RAM"," and barely loads the CPU. Over 4 hours of operation: 51 messages published, 3 subscribers, 4 active topics. A drop in the ocean.",[15,11102,11103],{},"For comparison: Prometheus + Alertmanager consume ~200 MB. Grafana takes another ~150 MB. ntfy with webhook integration covers 80% of monitoring use cases for a hobby project without all that infrastructure overhead.",[22,11105,5302],{"id":5301},[15,11107,11108,11109,11111],{},"ntfy is fast: from ",[73,11110,9921],{}," to working push notifications with minimal configuration. A Go binary, a systemd service, an HTTP API. Install it, configure Caddy, create a user — and you have your own notification backend that depends on no one.",[15,11113,11114],{},"For a self-hosted AI agent, it's a must-have. Monitoring, alerts, task notifications — all via one simple protocol.",[652,11116,9928],{},{"title":71,"searchDepth":118,"depth":118,"links":11118},[11119,11120,11121,11122,11123,11124,11125,11126,11127,11132,11133,11138,11139,11145,11151,11152],{"id":9995,"depth":118,"text":9996},{"id":10025,"depth":118,"text":10026},{"id":10173,"depth":118,"text":10174},{"id":8913,"depth":118,"text":10217},{"id":10257,"depth":118,"text":10258},{"id":10308,"depth":118,"text":10309},{"id":10443,"depth":118,"text":10444},{"id":10548,"depth":118,"text":10549},{"id":10672,"depth":118,"text":10673,"children":11128},[11129,11130,11131],{"id":10676,"depth":349,"text":10677},{"id":10691,"depth":349,"text":10692},{"id":10706,"depth":349,"text":10707},{"id":10722,"depth":118,"text":10723},{"id":10764,"depth":118,"text":10765,"children":11134},[11135,11136,11137],{"id":10768,"depth":349,"text":10769},{"id":9618,"depth":349,"text":10842},{"id":10881,"depth":349,"text":10882},{"id":10911,"depth":118,"text":10912},{"id":10944,"depth":118,"text":10945,"children":11140},[11141,11142,11143,11144],{"id":10948,"depth":349,"text":10949},{"id":10998,"depth":349,"text":10999},{"id":11010,"depth":349,"text":11011},{"id":11030,"depth":349,"text":11031},{"id":11068,"depth":118,"text":11069,"children":11146},[11147,11148,11149,11150],{"id":9874,"depth":349,"text":9875},{"id":9881,"depth":349,"text":9882},{"id":9888,"depth":349,"text":9889},{"id":9895,"depth":349,"text":9896},{"id":11092,"depth":118,"text":11093},{"id":5301,"depth":118,"text":5302},"How to set up your own ntfy push notification server: Caddy reverse proxy, access tokens, integration with an AI agent via webhooks. Monitoring, alerts, and an infinite loop trap.",{},"\u002Fblog\u002Fself-hosted-ntfy-notifications.en",{"title":9977,"description":11153},"blog\u002Fself-hosted-ntfy-notifications.en",[8636,3465,9971,9972,9973,680],"rn-9GUC1HQPKVpoPVMbQVtlo2UjaYvHsk5JUmKI7fKo",1781187249502]