Skip to content
←Return to Projects
Case Study

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

TypeScriptNext.js 15Node.jsSQLiteDrizzle ORMDPAPITelegram Bot APIDocker

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

Interaction Channels
Next.js 15 Web App (:3333)
Voice HUD (:4321)
Telegram Bot
Embed Bridge (:3234)
Gateway & Protocol
Private Grove API (:8431)
Device Control API
DPAPI Token Auth
Cognitive Engine ("The Grove")
Mind Core
Behavior & Persona
Memory Tiering (STM/MTM/LTM)
Skill Registry
Execution & Safety
Host Action Policy
Exact-Action Ledger
Backup & Rollback Ledger
Distributed Node Runtime
Windows Companion Daemon
Local File Executor
SQLite / Drizzle State

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.

  1. 1Implement automated regression tests for memory graph decay sweeps earlier in the development lifecycle.
  2. 2Consolidate SQLite schema migrations between better-sqlite3 and pg-schema under a single unified migration runner.
  3. 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.

icelegends-interactive-shell ~ [kiwi]
History: ↑ / ↓
kiwi@the-grove:~/voices$

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.

  • /devices listează PC-urile împerecheate ale utilizatorului (online/offline, aceeași fereastră de 15s pe last_seen_at ca 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 task write_file pe dispozitivul selectat — același DeviceControl.propose pe 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ă noul DeviceControl.cancel.
  • /tasks citește ultimele 5 rânduri din exact același DeviceControl.list din 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, iar src/devices/local-executor.ts impune asta printr-un linkSync atomic (EEXIST dacă 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_query nu au propria lor gardă chat.type/identitate din amonte, spre deosebire de calea de mesaje existentă — handleDeviceCallbackUpdate re-derivă de la zero chat.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.chatId se 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_id e 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 tabel telegram_webhook_updates deja partajat — nu prin import din calea de mesaje; vezi „Comis îngust" mai jos, motivul.)
  • Butoane vechi/străine. Fiecare callback_data poartă primele 16 caractere hex ale digest-ului propunerii; decide/cancel reverifică 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/handleDeviceCallbackUpdate apelează DeviceControl direct (server-side, același proces — precedentul din dashboard pentru asta e web/src/app/api/admin/tick/route.ts care apelează direct DeviceControl.reap), ceea ce ocolește complet apelurile proprii rateLimit() 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ă: /devices oferă doar dispozitive nerevocate; o selecție învechită scoate la iveală 404-ul existent al DeviceControl.propose; TTL-ul de 5 minute al task-ului (neschimbat de aprobare) și verificările proprii de stare/expirare ale decide sunt 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:

  • getOrCreateTelegramConversation căuta o conversație existentă după formatul vechi de id tg_<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 /start fără cod trimitea un răspuns folosind getLinkByChatId(chatId)?.token ?? '' — pentru orice chat niciodată legat (cazul comun pentru un /start simplu) 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ă deleteAllSessions din @/lib/chat-history, care nu fusese ea însăși comisă niciodată — versiunea comisă a lui chat-history.ts se oprea la deleteSession. 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ă munca verify:imports ca 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ă errMessage din @/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). /write e omologul structurat, onest, al acelui formular, nu un scurtcircuit sub el; inventarea unui NLP netestat care emite acțiuni write_file la 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 în telegram.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. /tasks citeș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 în POST /api/devices/agent/result), nu ghicit aici.
  • Paritate în dashboard pentru cancel. Capacitatea DeviceControl.cancel/POST /api/devices/tasks/cancel există la nivel de platformă și e livrată în Telegram, dar devices-panel.tsx din 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.

~ End of Document ~