VINCI für Mac: Was v2.4.0 jetzt kann, und warum das kein Chatbot mehr ist

VINCI v2.4.0 für macOS: Outline-Migration, Augen und Hände, 7 proaktive Daemons, Memory-Auto-Lernen. Dev-Log zu einem persönlichen KI-Assistenten, der wirklich eigenständig arbeitet.

Ich baue VINCI seit Jänner2026 als meinen persönlichen KI-Assistenten für macOS. Electron, React 18, Gemini 2.5 Flash als LLM, Three.js für den goldenen Partikel-Orb. Kein SaaS-Abo, kein Cloud-Vendor-Lock-in, keine fremde UI. Nur ich, mein MacBook Air M4, und eine App, die sich über Monate in etwas verwandelt hat, das ich mir vor Monaten nicht hätte vorstellen können.

v2.4.0 ist live. Und ich will ehrlich sein: das ist der Build, bei dem ich zum ersten Mal das Gefühl hatte, nicht mehr mit einem Chatbot zu reden.

Was vor v2.4.0 schon drin war

Kurze Einordnung, damit der Rest Sinn ergibt.

VINCI v2.1.2 hatte bereits die JARVIS-Architektur in Gang gesetzt: Intent-Router (J1), heuristisches Modell-Routing zwischen Flash und Pro (J2), Episodic Context im System-Prompt (J3), proaktive Daemons für Kalender-Warnings und Strom-Anomalien (J4), und einen Self-Eval-Loop der jede Antwort asynchron bewertet (J5). Dazu drei Sub-Agents: Researcher, Briefing, Weekly-Review.

478 Tests, Obsidian-Vault als Knowledge-Graph, WordPress-Importer für digitalhandwerk.rocks.

Das war schon viel. Aber es hatte einen konzeptionellen Riss: das Wissen lebte im Obsidian-Filesystem, und der Auto-Firma-Detection-Regex hat mir trotz aller Fixes immer wieder tausende deutscher Allerweltswörter als „Firmen-Entitäten“ in den Vault geschrieben. Nach dem letzten Aggressive-Cleanup waren es 8.380 Müll-Stubs. Danach 22.

Das Grundproblem war nicht lösbar ohne Architektur-Änderung.

Outline statt Obsidian

Das ist die größte strukturelle Entscheidung. Das komplette Knowledge-Management ist von Obsidian-Filesystem auf meine self-hosted Outline-Instanz umgezogen.

Warum das nicht trivial war: Outline kennt keine [[Wikilinks]]. Interne Links sind echte Markdown-Links mit absoluten URLs, und nur absolute URLs erzeugen Backlinks. Das bedeutete einen neuen linkifyMarkdown-Layer, der Wikilink-Syntax auflöst, fehlende Entity-Stubs anlegt, und dabei einen 2-Stufen-Filter durchläuft: erst eine looksLikeProperNoun-Heuristik, dann ein Gemini-Flash-Klassifizierungsaufruf mit 15-Minuten-Session-Cache gegen Duplikat-Stürme.

9 Collections: Personen, Tiere, Firmen, Orte, Themen, Quellen, RSS, Notizen, Briefings.

Was damit rausgeflogen ist: obsidian.js, obsidianGraph.js, _wikilinkEngine.js, graphCleaner.js, _vaultMigration.js, _brokenLinkCleaner.js, _aliasBuilder.js, _curatorApply.js. Insgesamt 13 Test-Files weg. Das Codebase ist merklich schlanker. Und das Firm-Müll-Problem ist strukturell gelöst, weil Firmen-Stubs jetzt ausschließlich durch den Researcher-Sub-Agent entstehen, nie mehr durch eine Regex-Heuristik.

Memory-Auto-Lernen ist zurück

Der alte memoryWorker war bei der Outline-Migration ausgebaut worden, was bedeutete: VINCI hat nach dem Gespräch nichts mehr gelernt. Kein gutes Gefühl.

Neu gebaut: 30 Sekunden Debounce nach jedem Chat, dann 2 Minuten Cooldown. Der Worker extrahiert stabile Fakten aus den letzten 12 Messages. Zuerst lokal via Ollama (gemma3:4b), bei Ausfall Fallback auf Gemini Flash. Die erprobten Heuristiken aus dem alten Worker sind wiederverwendet: looksLikeFact, stripSystemNoise, isDuplicate, Tainted-Filter. Schreiben via saveFact dual: lokaler SQLite-Cache und Outline-Entity-Note.

Das ist kein „Ich speichere alles“-Ansatz. Der Tainted-Filter blockiert, dass System-Metriken oder Web-Suchinhalte als persönliche Fakten enden. „Mac CPU 24%“ ist kein Fakt über mich.

7 proaktive Daemons

Vorher: 3 Daemons (Calendar-Warning, Strom-Anomaly, Vault-Drift). Jetzt 7.

Neu dazu:

important-mail: Alle 10 Minuten tagsüber bewertet Gemini ungelesene Mails und meldet nur, was wirklich wichtig ist. 18-Stunden-Cooldown pro Mail. Kein Spam-Notify.

morning-push: 07:30 Uhr, ein Einzeiler. Nächster Termin plus dringendste Aufgabe. Kein Briefing, nur der eine Satz, der zählt.

meeting-prep: Etwa 30 Minuten vor Terminen mit einer bekannten Person oder Firma zieht VINCI Kontext aus Outline via outline_search. Ich gehe in kein Meeting mehr kalt.

task-due: 09:00 und 16:00, alle heute fälligen oder überfälligen Reminders. Dafür musste reminders.getAll um dueInSec erweitert werden, locale-unabhängig via AppleScript-Datumsdifferenz, weil macOS-Datumsstrings je nach Spracheinstellung anders aussehen.

Und: vault-drift plus quarantine-reminder sind raus. Obsidian-Altlast, nicht mehr nötig.

Augen

vision.js. Ein neues Tool: vision_lookAtScreen({question, scope}).

VINCI macht via screencapture einen Screenshot des ganzen Screens oder des aktiven Fensters, schickt ihn direkt an Gemini Flash multimodal, bekommt eine Text-Antwort zurück. Der Screenshot wird sofort gelöscht, nie gespeichert.

„Was steht in dem Fenster rechts?“ funktioniert jetzt.

Hände

control.js. Das ist das Modul, bei dem ich am längsten über die Permission-Architektur nachgedacht habe.

VINCI kann jetzt: Apps öffnen und beenden, URLs öffnen (via macOS open, braucht keine Automation-Freigabe), Dateien lesen, schreiben, verschieben, löschen, AppleScript und Shell-Kommandos ausführen, und UI-Automation: Tippen, Tastenkürzel senden, Klicken.

Das ist eine Menge Macht für eine App, die auf meinem persönlichen Mac läuft.

Die Permission-Schicht: warum das nicht optional ist

_permissions.js ist der Kern, über den ich mir die meisten Gedanken gemacht habe.

Alles ist default AUS. 5 Fähigkeiten einzeln schaltbar im neuen Settings-Tab „Steuerung“: apps, files, scripting, uiAutomation, vision.

Riskante Aktionen triggern einen nativen macOS-Bestätigungsdialog (dialog.showMessageBox). Der ist abschaltbar über confirmRisky, aber default an.

Datei-Operationen sind auf eine fileRoots-Allowlist beschränkt. Und hier ist der Part, der beim Code-Review wichtig war: pathAllowed löst Symlinks via realpath auf, bevor es gegen die Allowlist prüft. Ohne das kann ein Symlink in einem erlaubten Ordner auf einen beliebigen Pfad außerhalb zeigen. Das wäre ein klarer Allowlist-Ausbruch. Mit realpath nicht möglich. Regressionstest vorhanden.

Der Confirm-Dialog ist fail-safe gebaut: jeder Fehler, jedes fehlende Fenster führt zu Ablehnen. Nie fail-open.

osaEscape strippt Newlines aus App-Namen und getipptem Text, bevor sie in AppleScript-Strings landen. Newlines können AppleScript-Statements vorzeitig beenden und neue beginnen. Das ist eine klassische Injections-Oberfläche, die man einmal schließen muss.

Audit-Log läuft über die Telemetry mit drei States pro Aktion: allowed, blocked, denied.

Was 325 Tests tatsächlich bedeuten

Das klingt nach einer Zahl die man hinschreibt. Aber hinter den 325 Tests sind 6 Regression-Sentinels, die ich besonders mag, weil sie spezifische Bugs reproduzieren, die ich live erlebt habe.

sentinel-geminiToolMerge: Forced Tools müssen immer eine Subset der deklaratierten Tool-Declarations sein. Wenn nicht, gibt Gemini einen 400-Fehler zurück. Dieser Sentinel fängt das ab, bevor es im Briefing-Button live crasht.

sentinel-calendarGracefulError: Der Calendar-Code darf nie werfen. Er gibt {events: [], error} zurück, und der Briefing-Agent rendert das ehrlich als „Kalender nicht erreichbar“, nicht als leere Terminliste.

sentinel-noUTCTimestamps: Verbietet new Date().toISOString() in 13 user-sichtbaren Modulen. Alle lokalen Zeitstempel gehen über _localTime.js mit Vienna-Offset und DST-Awareness. Wer eine solche Zeile einbaut, bekommt sofort roten Build.

Der Calendar-Daemon hat im Dev-Modus AppleScript jede 2 Minuten ohne TCC-Permission aufgerufen und dabei tccd CPU-Spitzen verursacht. Proactive-Daemons schalten sich jetzt nach 3 aufeinanderfolgenden Fehlern selbst ab bis zum App-Restart.

JARVIS-Level: 75 Prozent

J1 bis J5 sind vollständig live. J6 (Sub-Agents) ist in Stufe 0 bis 4 fertig: Job-Queue mit persistentem JSON-Store, Lifecycle-Management, Concurrency-Limit, Event-System, Researcher-Agent, Briefing-Agent, Weekly-Review.

Offen: J6 Stufe 5 (Vault-Curator), J7 (Pattern-Learning), J8 (Wake-Word „VINCI“).

Ich bin mir nicht sicher, in welcher Reihenfolge ich die angehe. Wake-Word reizt mich sehr, weil es den Hotkey ersetzt und die Interaktion natürlicher macht. Porcupine von Picovoice ist mein Favorit für die Engine, weil Custom-Wake-Word ohne Training möglich ist und die Integration in Electron-Subprocess überschaubar bleibt. Aber das ist noch nicht entschieden.

Was mich dabei überrascht hat

Ich hätte gedacht, die Outline-Migration wäre der schwierigste Teil dieses Builds. War sie nicht.

Der schwierigste Teil war, die Memory-Worker-Extraktions-Logik so zu tunen, dass tainted Inhalte zuverlässig rausgefiltert werden, ohne dass echte Fakten verschwinden. Die Grenze ist feiner als sie aussieht. „VINCI hat Gemini Flash als Haupt-LLM“ ist ein Fakt. „Gemini Flash antwortet manchmal mit 0 Content-Parts“ ist System-Noise. Der Unterschied liegt nicht im Format des Satzes, sondern im Kontext, aus dem er stammt.

stripSystemNoise entfernt jetzt CPU-, RAM-, und Akku-Zeilen aus der Konversation, bevor der LLM sie überhaupt sieht. Der Memworker extrahiert auf einem bereinigten Konversations-Snapshot. Das klingt einfach. Bis man realisiert, wie viele verschiedene Formate System-Output haben kann.

Das Repo ist weiter privat und wird es auch bleiben. Das DMG hat 118 MB, kein Code-Signing (Gatekeeper-Warning beim ersten Start ist bekannt und gewollt, bis ich das Apple-Developer-Zertifikat hole).

Du kannst warten, bis VINCI fertig ist. Aber ich glaube nicht, dass es ein „fertig“ geben wird.

share that. please.

LinkedIn
Facebook
Threads
Telegram
WhatsApp
Email
Alex Januschewsky, KI-Berater und Prompt Rocker
Über den Autor
Alex Januschewsky
KI-Berater & Prompt Rocker // Salzburg, AT

Ich arbeite seit 1989 in Kommunikation und Werbung, heute als KI-Berater unter der Marke Prompt Rocker. Ich baue Dinge selbst: mit Claude Code, n8n, WordPress und Coolify auf Hetzner. Vibe Coding ist für mich kein Begriff, sondern tägliche Praxis. Auf vibecraft.rocks dokumentiere ich, was dabei entsteht.

ship safe.