KIWI
Autonomous local AI agent and community of voices living in The Grove with private memory.
The Problem
Autonomous AI agents often suffer from leaky memory isolation, unpredictable side-effects on host systems, and lack of human-in-the-loop governance for high-stakes file operations and external device actions.
My Role
Architected and implemented the cognitive engine ("The Grove"), fail-closed host action safety policies, immutable exact-action plan ledgers, multi-device agent companion protocol, and unified multi-channel gateways.
Highlights
- Community of six voices living in The Grove with local memory running on the user's machine
- Local cognitive core ("The Grove") separating owner memory from credentialed client sessions
- Fail-closed security policy requiring immutable exact plans with rollback for all destructive actions
- DPAPI-secured Windows companion agent with local execution receipts and anti-re-execution guarantees
- Multi-modal interaction surface: Next.js Web UI, Voice HUD (:4321), and Telegram bot gateway with inline approvals
- Deterministic audit ledger tracking every state mutation, file movement, and memory association
Stack
Constraints
- Host safety is non-negotiable: no autonomous file deletion or batch reorganization without human-confirmed exact plans.
- Offline-first engine: core cognitive loops and memory databases must run purely locally without cloud dependencies.
- Multi-tenant memory isolation: administrative sessions and API token holders must never access the owner private mind context.
System Architecture
Key Trade-offs
The decisions worth defending — what I chose, what I turned down, and why.
File management execution model
Chose
Immutable two-phase exact-action plans with hash verification and dry-run preview
Rejected
Direct agent tool execution with broad shell permissions
Agents making speculative shell or file modifications risk catastrophic data loss; two-phase plans guarantee human oversight and rollback capability.
Engine-to-web communication architecture
Chose
Dedicated loopback HTTP/SSE gateway with bearer token authentication
Rejected
Monolithic single-process Next.js application
Decoupling the AI engine process from the Next.js frontend ensures background monitoring, telegram polling, and long-running skills survive web redeployments.
Device pairing security model
Chose
Windows DPAPI machine-bound tokens with polling architecture
Rejected
Inbound SSH or remote socket servers on client machines
Inbound listening ports expose clients to local network scans; outbound HTTPS polling behind DPAPI keeps remote companion devices secure.
What I'd Do Differently
An honest retrospective — the stuff I'd change with more time, more users, or a second pass.
- 1Implement automated regression tests for memory graph decay sweeps earlier in the development lifecycle.
- 2Consolidate SQLite schema migrations between better-sqlite3 and pg-schema under a single unified migration runner.
- 3Add fine-grained telemetry dashboards to observe cognitive latency across multi-turn reasoning chains.
Interactive Autonomous Agent Shell
Test live KIWI autonomous agent supervisor routines (agent status, grove ping, mcp tools, help) in the emulated shell below.
Technical Deep-Dive
Architecture, specifications, and implementation details.
P3 — Selecție dispozitiv și aprobare editări de fișiere prin Telegram
Revizia 26, 2026-09-09. Felie aleasă la cererea explicită de a continua cu P3, deși două treceri anterioare de planificare (reviziile 22 și 25) o amânaseră ca „prea mare pentru o singură felie" — o a treia trecere Opus a găsit că acea estimare exagera golul: DeviceControl.propose/decide (P1) implementează deja contractul aprobare/respingere cerut de plan; ce lipsea de fapt era doar transportul prin chat (gestionarea callback_query/tastaturi inline, complet absentă din webhook). Cod local și verificare automată; KIWI_DEVICES_ENABLED rămâne dezactivat implicit, fără lansare în producție.
#Ce livrează, față de linia literală de acceptare a planului
"Acceptance: a Telegram request executes only on the linked selected PC after its required approval, and results match the web/TUI task record.
/deviceslistează PC-urile împerecheate ale utilizatorului (online/offline, aceeași fereastră de 15s pelast_seen_atca dashboard-ul) cu o tastatură inline; atingerea unuia persistă selecția pentru acel chat (coloană nouătelegram_links.selected_device_id) și e confirmată prin editarea mesajului pe loc./write <cale>urmat de conținutul exact al noului fișier, începând pe linia următoare, propune un taskwrite_filepe dispozitivul selectat — acelașiDeviceControl.proposepe care îl apelează și formularul manual din dashboard — afișat cu butoane ✅ Approve / ❌ Reject.- Aprobarea apelează
DeviceControl.decide, apoi editează mesajul într-o confirmare statică cu un buton 🚫 Cancel, activ până când dispozitivul preia task-ul; respingerea apeleazădecide(approve:false); anularea (un gol real găsit în timp ce construiam asta, mai jos) apelează noulDeviceControl.cancel. /taskscitește ultimele 5 rânduri din exact acelașiDeviceControl.listdin care randează și dashboard-ul — „rezultatele se potrivesc cu înregistrarea task-ului din web/TUI" din linia de acceptare, prin construcție, nu printr-o implementare paralelă separată.- Creează DOAR fișiere noi (
expectedSha256: null) — Telegram n-are cum să dovedească hash-ul curent al unui fișier, iarsrc/devices/local-executor.tsimpune asta printr-unlinkSyncatomic (EEXISTdacă fișierul a apărut între timp), nu doar prin faptul că opțiunea nu e oferită.
#Listă de verificare securitate
- Autorizare. Update-urile
callback_querynu au propria lor gardăchat.type/identitate din amonte, spre deosebire de calea de mesaje existentă —handleDeviceCallbackUpdatere-derivă de la zerochat.type === 'private' && chat.id === from.idînainte de a atinge orice, deci doar chat-ul privat propriu al contului legat poate acționa vreodată pe propriile butoane. Chat-urile de grup sunt excluse atât la trimitere (link.chatIdse scrie doar din interiorul aceleiași gărzi), cât și la recepție. - Reluare (replay). Apăsările de butoane folosesc același mecanism de deduplicare
(chat_id, update_id)pe care se bazează deja calea de mesaje —update_ide strict crescător per bot pe toate tipurile de update, deci o coliziune între tipuri e imposibilă. (Implementat ca propria verificare locală, mică, pe același tabeltelegram_webhook_updatesdeja partajat — nu prin import din calea de mesaje; vezi „Comis îngust" mai jos, motivul.) - Butoane vechi/străine. Fiecare
callback_datapoartă primele 16 caractere hex ale digest-ului propunerii;decide/cancelreverifică faptul că digest-ul CURENT al task-ului începe încă cu acel prefix înainte de a acționa, deci un buton randat pentru o propunere între timp schimbată sau deja decisă nu poate acționa tăcut pe ce a devenit task-ul. - Limite de rată.
handleDeviceCommand/handleDeviceCallbackUpdateapeleazăDeviceControldirect (server-side, același proces — precedentul din dashboard pentru asta eweb/src/app/api/admin/tick/route.tscare apelează directDeviceControl.reap), ceea ce ocolește complet apelurile propriirateLimit()ale rutei/api/devices. Găsit și reparat în timp ce construiam asta: Telegram aplică acum EXACT aceleași chei și plafoane per cont ca ruta HTTP (device-tasks,device-tasks-decision,device-tasks-cancel) — fără asta, Telegram n-ar fi avut nicio limitare pe propuneri/decizii, peste bugetul propriu al dashboard-ului. - Dispozitive revocate/expirate. Eșuează sigur fără nicio logică nouă:
/devicesoferă doar dispozitive nerevocate; o selecție învechită scoate la iveală 404-ul existent alDeviceControl.propose; TTL-ul de 5 minute al task-ului (neschimbat de aprobare) și verificările proprii de stare/expirare aledecidesunt aceleași pe care se bazează deja dashboard-ul.
#Un gol real în DeviceControl, găsit și reparat în timp ce construiam asta
Linia de acceptare P3 din plan cere „approve/reject/cancel". DeviceControl.decide accepta doar starea awaiting_approval — nu exista nicio cale de a abandona un task deja aprobat, dar încă nepreluat de un dispozitiv, decât revoke() (care omoară tot dispozitivul, nu doar un task). Adăugat cancel(userId, id, digest): abandonează awaiting_approval sau approved, refuză running sau orice stare terminală. Expus la POST /api/devices/tasks/cancel (aceeași formă ca tasks/decision) și din butonul post-aprobare din Telegram — asta închide golul pentru ambele interfețe, nu doar pentru cea nouă.
Un al doilea bug, independent, găsit în același fișier: propose() număra task-urile pendinte față de plafonul de 20 per dispozitiv și apoi insera rândul nou ÎN AFARA unei tranzacții, spre deosebire de fiecare altă metodă din clasă. Două propuneri concurente ajunse la plafon puteau trece amândouă verificarea înainte ca vreuna să insereze. Încadrat acum în db.transaction(...), ca restul.
#Comis îngust, intenționat
web/src/lib/telegram.ts și ruta webhook sunt ele însele fișiere mari, preexistente, necomise — muncă din era P0 pe Telegram (deduplicare, cotă, gestionare fișiere, preferințe) lăsată necomisă printr-o alegere explicită, permanentă (vezi lista Blocked/next din CLAUDE.md, care numește explicit „Telegram"). Toată logica nouă P3 stă într-un modul nou, de sine stătător, web/src/lib/telegram-devices.ts, exact ca cele două fișiere fierbinți să aibă nevoie doar de câteva hunk-uri mici, izolate (o linie schimbată, patru helper-e mici de Bot API adăugate la final, o extindere de tip, o ramură nouă, un apel nou) — construite din baza REAL COMISĂ la HEAD, într-o copie izolată în scratch, nu din copia de lucru, și stagiate cu git hash-object/git update-index. Confirmat prin git diff --cached că cele două diff-uri sunt exact liniile legate de P3 (99 în total) și nimic din bucket-ul acela pendinte de ~500 de linii.
O consecință: telegram-devices.ts NU importă deliberat isTelegramUpdateDuplicate, încă necomis — are propria verificare locală de deduplicare, de câteva linii, pe același tabel telegram_webhook_updates deja comis (din revizia 12), deci acest modul n-are nicio dependență de starea de commit a acelei funcții pendinte.
Alte două bug-uri au ieșit la iveală citind acel bucket pendinte pentru munca asta, reparate în copia de lucru și verificate prin teste, dar lăsate INTENȚIONAT necomise, alături de restul bucket-ului:
getOrCreateTelegramConversationcăuta o conversație existentă după formatul vechi de idtg_<chatId>, dar mereu INSERA folosind un id hashuit — ramura „reutilizează o conversație existentă" nu se putea potrivi niciodată cu un rând real, moartă tăcut de când a fost scrisă.- Handler-ul pentru
/startfără cod trimitea un răspuns folosindgetLinkByChatId(chatId)?.token ?? ''— pentru orice chat niciodată legat (cazul comun pentru un/startsimplu) asta e o trimitere garantat eșuată, cu token gol, iar pentru un proprietar deja legat trimitea mesajul greșit, derutant, „aștept posesorul".
#Verificare post-commit: un clone izolat a găsit alte două regresii reale, nelegate
Urmând aceeași disciplină ca revizia 23, după comiterea acestei felii am rulat un git clone --local --no-hardlinks proaspăt + npm ci + npx tsc --noEmit pe noul HEAD. Niciunul dintre cele două commit-uri P3 n-a produs vreo eroare — dar checkout-ul proaspăt tot n-a trecut typecheck-ul, din motive care s-au dovedit anterioare acestei revizii:
web/src/app/api/user/data/route.ts(comis la revizia 24) importădeleteAllSessionsdin@/lib/chat-history, care nu fusese ea însăși comisă niciodată — versiunea comisă a lui chat-history.ts se oprea ladeleteSession. Verificarea proprie a reviziei 24, din această sesiune, a verificat doar copia de lucru (care avea deja funcția pendinte) — exact orbirea pe care există toată muncaverify:importsca s-o închidă, doar că aici dependența nu era un import relativ, deci garda aceea n-a prins-o.web/src/app/api/admin/tick/route.ts(comis mai devreme în această sesiune) importăerrMessagedin@/lib/errors, un utilitar mic, netracked, fără nicio dependență.
Ambele reparate și reverificate în același clone izolat (git fetch + rerulare tsc --noEmit): singurele erori rămase pe checkout proaspăt sunt în web/src/lib/stripe/index.ts și web/src/lib/studio.ts, ambele deja șterse în copia de lucru ca parte a unei migrări Stripe→LemonSqueezy/sistem-de-persona în curs, deliberat necomisă — neatinse aici, nefăcând parte din P3.
Asta înseamnă că npm run typecheck:web eșuează ACUM pe un checkout cu adevărat proaspăt al lui master, independent de orice din această revizie — merită atenția operatorului înainte de următoarea rulare CI sau checkout pe mașină curată, fiindcă stripe/index.ts/studio.ts sunt încă importate de ruta webhook comisă la HEAD (sistemul vechi de persona), deci simpla lor ștergere ar strica webhook-ul comis acum, nu l-ar repara. Reparația reală înseamnă comiterea unei părți mai mari din migrarea de persona/facturare aflată în curs — în afara scopului acestei revizii, prin aceeași alegere permanentă care a amânat restul bucket-ului Telegram.
[REZOLVAT la rev 36] Stripe/studio au fost șterse definitiv (plățile rulează pe LemonSqueezy) și fiecare importator real a fost migrat la noul sistem owner-chat/chat-attachments/chat-body în același commit. npm run typecheck:web e curat pe checkout proaspăt de atunci. Paragraful de mai sus rămâne cum a fost scris la momentul respectiv, nerescris.
#Non-obiective deliberate pentru această felie
- Voce. Nu există nicio conversie voce-text nicăieri în aplicația web.
- Redactare de task-uri mediată de NLP („vorbești natural și Kiwi propune editarea"). Nicio interfață n-are asta azi — dashboard-ul web e un formular manual (dropdown dispozitiv, câmp cale, textarea conținut).
/writee omologul structurat, onest, al acelui formular, nu un scurtcircuit sub el; inventarea unui NLP netestat care emite acțiuniwrite_filela nimereală ar fi mișcarea nesigură aici. - Conținut peste limita proprie de ~4096 caractere a unui mesaj Telegram. Plafonul de 64 KiB al protocolului rămâne neschimbat; transportul e limita. Urmare ieftină, documentată: acceptarea unui atașament document al cărui caption e
/write <cale>, reutilizând funcțiile de gestionare fișiere deja existente întelegram.ts. - Un flux de propunere stateful, pe mai multe mesaje („trimiți calea, apoi trimiți conținutul separat"). Evitat deliberat — ar avea nevoie de propria mașină de stări pentru input pendinte și propriul TTL. Un mesaj, o propunere.
- Trimitere proactivă a rezultatelor.
/taskscitește starea live în loc. Trimiterea proactivă ar avea nevoie să știe CARE interfață a propus un task (nimic nu urmărește asta azi, între Telegram și dashboard-ul web) ca să nu scurgă în Telegram rezultatul unui task pornit din web — un gol real, specificat pentru o urmare (un tabel mic „origine task" plus un hook fire-and-forget înPOST /api/devices/agent/result), nu ghicit aici. - Paritate în dashboard pentru
cancel. CapacitateaDeviceControl.cancel/POST /api/devices/tasks/cancelexistă la nivel de platformă și e livrată în Telegram, dardevices-panel.tsxdin dashboard-ul web a rămas neatins — e el însuși un fișier P1 preexistent, netracked, iar adăugarea la el acum ar însemna comiterea și a restului conținutului lui pendinte, nelegat.
#Verificări
npm run typecheck și npm run typecheck:web: ambele verzi. Motor: 142 teste (141 trecute + 1 skip semantic opțional; neschimbat). Web/mind: 40 suite / 264 teste (+15: 12 noi în web/tests/telegram-devices.test.ts, 2 noi în device-control.test.ts pentru cancel, 1 nou în device-api.test.ts pentru cablarea HTTP tasks/cancel). npm run verify:imports: 490 fișiere, zero probleme, zero pachete nedeclarate. Clone izolat + npm ci proaspăt + npx tsc --noEmit: curat, cu excepția golului preexistent, nelegat, de migrare stripe/studio documentat mai sus. Fără lansare în producție.
#Ce rămâne din P3
Voce; redactare de propuneri mediată de NLP; conținut mai mare decât un mesaj Telegram; trimitere proactivă a rezultatelor (specificată mai sus); buton cancel în dashboard; acceptare reală pe două conturi/dispozitive (încă blocată de lipsa unui al doilea PC Windows fizic, la fel ca P1); golul de typecheck stripe/studio preexistent, scos la iveală de această revizie, dar nereparat aici — rezolvat la rev 36 (vezi mai sus).
Surse: web/src/lib/telegram-devices.ts, web/src/lib/telegram.ts, web/src/app/api/telegram/webhook/route.ts, web/src/lib/device-control.ts, web/src/app/api/devices/[[...path]]/route.ts, web/src/lib/db/{sqlite-schema,pg-schema,index}.ts, pilotul P1, checkpoint.