Кейсы и практика

Ambient AI для Mac: Tauri + Swift + Ollama. Построил, протестировал, закрыл

Что такое ambient AI?

Ambient AI — категория программ, которые пассивно наблюдают за поведением пользователя в фоновом режиме: отслеживают активные приложения, паттерны сессий и переключения между задачами — и предлагают инсайты или автоматизации без явных запросов. В отличие от ассистентов, ambient AI работает непрерывно и незаметно, анализируя паттерны через локальную LLM и сохраняя все данные на устройстве.

TL;DR

  • -Стек: Swift daemon (NSWorkspace + Accessibility API) → Tauri 2.0 Rust backend → React popup; SQLite WAL + Ollama llama3.2:3b, без облака
  • -Tauri потребляет 30–40 МБ RAM против 150–300 МБ у Electron — критично для фонового процесса, работающего весь день
  • -За 5 дней реального использования: 3 445 сессий и критический баг в собственном коде обнаружения паттернов
  • -ChatGPT и Gemini оба оценили проект как 'закрыть': privacy-first ambient AI не имеет чёткого пути монетизации в 2025 году
  • -Проект open source на GitHub (spyrae/ambientghost) — архитектура и постмортем задокументированы полностью

Ghost - macOS menu bar приложение. Тихо наблюдает за работой, хранит всё локально, анализирует паттерны через локальный LLM. Swift daemon, Rust backend на Tauri 2.0, React popup, SQLite, Ollama. Никакого облака, никакой телеметрии.

За 5 дней реального использования Ghost собрал 3 445 сессий и нашёл критический баг в собственном коде. Потом я скормил проект ChatGPT и Gemini с просьбой оценить его как VC - оба сказали закрывать. Я согласился.

Дальше - технический разбор архитектуры и честный постмортем. Код открыт на GitHub.


Идея: невидимый AI-ассистент

Задача звучала красиво. Приложение живёт в menu bar, наблюдает за тем, какие приложения ты используешь, находит повторяющиеся паттерны и предлагает автоматизации. Без ручных настроек, без таймеров, без дашбордов, которые надо открывать.

Четыре уровня:

  1. Awareness - живой пульс: какое приложение активно, сколько переключений за день
  2. Nudges - «ты 25 минут в Telegram, вернуться в Warp?», напоминание о перерыве, вечерний wrap-up
  3. Insights - сравнение недель, пиковые часы, какие приложения используются вместе
  4. Automations - LLM находит паттерн «ты каждый вечер открываешь Telegram → Perplexity» и предлагает автоматизировать

Всё локально. SQLite, Ollama, ноль сетевых запросов (кроме опционального Claude API как fallback).


Стек и архитектура

Swift Daemon (GhostDaemon)
  NSWorkspace + Accessibility API
  NWListener WebSocket :9876

        │ JSON events

Rust Backend (Tauri 2.0)
  observer.rs  → сессии, переходы
  db.rs        → SQLite (WAL, 30-day rolling)
  patterns.rs  → n-gram агрегация
  llm.rs       → Ollama → Claude fallback
  nudges.rs    → focus drift, break, wrap-up
  runner.rs    → open app / URL / Shortcut

        │ Tauri IPC

React 19 + Tailwind (popup 320×480)

Почему такой стек

Tauri 2.0 вместо Electron - Ghost должен быть незаметным. Electron в menu bar жрёт 150-300 МБ RAM. Tauri - 30-40 МБ. Для фонового процесса, который работает весь день, разница принципиальная. У Tauri 104 000 звёзд на GitHub, на нём построены ChatGPT Desktop (54 400 звёзд), Jan (41 100), GitButler (19 900).

Swift daemon отдельно от Tauri - ограничение платформы. NSWorkspace и Accessibility API требуют нативного macOS-процесса. Tauri 2.0 позволяет подключать Swift-плагины, но для постоянного наблюдения нужен отдельный daemon. Связь через WebSocket на localhost:9876.

Ollama (llama3.2:3b) для анализа паттернов - privacy. Данные о том, какие приложения ты используешь, не должны уходить на внешний сервер. Ollama сейчас - 166 000 звёзд, 52 миллиона загрузок в месяц. llama3.2:3b хватает для классификации паттернов из 5-10 переходов.

Swift daemon: как наблюдать за macOS

Ядро - NSWorkspace.didActivateApplicationNotification. При каждом переключении приложения macOS отправляет нотификацию. Daemon ловит её, забирает window title через Accessibility API и шлёт JSON по WebSocket:

@objc private func appDidActivate(_ notification: Notification) {
    guard let app = notification.userInfo?[
        NSWorkspace.applicationUserInfoKey
    ] as? NSRunningApplication else { return }
    sendEvent(app: app)
}

private func sendEvent(app: NSRunningApplication) {
    let bundleId = app.bundleIdentifier ?? "unknown"
    guard bundleId != lastBundleId else { return } // дедупликация
    lastBundleId = bundleId

    let windowTitle = getWindowTitle(for: app) // AXUIElement
    let event: [String: Any] = [
        "app_bundle_id": bundleId,
        "app_name": app.localizedName ?? "Unknown",
        "window_title": windowTitle,
        "timestamp": isoFormatter.string(from: Date()),
    ]
    server.broadcast(jsonString)
}

Window title берётся через AXUIElement. Без Accessibility permission вернёт пустую строку, но трекинг приложений продолжит работать:

func getWindowTitle(for app: NSRunningApplication) -> String {
    guard AXIsProcessTrusted() else { return "" }
    let axApp = AXUIElementCreateApplication(app.processIdentifier)
    var focusedWindow: AnyObject?
    AXUIElementCopyAttributeValue(
        axApp, kAXFocusedWindowAttribute as CFString, &focusedWindow
    )
    // ... извлекаем kAXTitleAttribute
}

WebSocket сервер - Network.framework (NWListener). Никаких внешних зависимостей, чистый Foundation + AppKit.

Rust backend: сессии и паттерны

Rust-сторона подключается к daemon как WebSocket-клиент с автореконнектом каждые 5 секунд. Каждое событие - новая сессия в SQLite:

fn handle_app_event(event: AppEvent) {
    // Закрыть предыдущую сессию (если > 30 сек)
    if let (Some(sid), Some(start_ts)) = (...) {
        let duration = (now_ts - start_ts).max(0) as u64;
        if duration >= 30 {
            db::end_session(sid, &now_iso, duration).ok();
        }
    }
    // Записать переход
    db::insert_transition(prev_sid, &event.app_bundle_id, ...);
    // Начать новую сессию
    db::insert_session(&new_id, &now_iso, &event.app_bundle_id, ...);
}

Idle detection - каждые 30 секунд проверяем, было ли событие за последние 2 минуты. Нет - закрываем сессию.

Паттерны: n-gram агрегация + LLM

Раз в час patterns.rs берёт переходы за 7 дней, группирует по временным окнам (morning/midday/afternoon/evening/night), извлекает пары приложений и считает, в скольких днях эта пара встречалась. Фильтр - минимум 2 дня.

Кандидаты уходят в Ollama:

Observed pattern (repeated 4 times, typically at 22:00 evening):
1. Telegram (ru.keepcoder.Telegram)
2. Perplexity (ai.perplexity.comet)

Respond ONLY in JSON: { "title": "...", "confidence": 0.0-1.0, ... }

Trust model: максимум 1 suggestion в день, порог confidence 0.75. Принял, отложил или отклонил - решение записывается, повторно Ghost не покажет.


Данные: 5 дней реального использования

Ghost работал с 18 по 22 марта. Сырые числа:

МетрикаЗначение
Сессий3 445
Уникальных приложений53
Переходов3 299
Паттернов (LLM)3

Топ по времени:

ПриложениеВремяСредняя сессия
Warp (терминал)15 сек
Perplexity Comet2.3ч12 сек
Dia (браузер)1.3ч13 сек
Telegram8 сек

Пик активности - 13-15ч и 21-23ч. Разрыв в 3-8 утра. Типичный ритм remote-разработчика.

Три обнаруженных паттерна - все «вечерний ресёрч»: Telegram → Perplexity или Warp → Perplexity около 22-23ч. Confidence 0.95 при 3-4 повторениях. LLM их корректно нашёл. Ценность - ноль. Я и так знаю, что вечером сижу в Perplexity.

Баг, который сломал 80% данных

Из 3 445 сессий у 2 778 (80.6%) - duration_seconds = 0 и ended_at = NULL.

Причина в observer.rs. Сессия закрывается только при duration >= 30:

if duration >= 30 {
    db::end_session(sid, &now_iso, duration).ok();
}

Переключился за 15 секунд - сессия создалась, но никогда не закрылась. Висит с нулевой длительностью навсегда. Ирония: средняя реальная сессия - 8-15 секунд. Люди не работают блоками по полчаса. Они alt-tabают сотни раз в день.

ДеньВсегоСломанныхРабочих
18 марта316268 (85%)48
19 марта1 3131 015 (77%)298
20 марта339289 (85%)50
21 марта645545 (85%)100
22 марта832661 (79%)171

Фикс простой - убрать порог и закрывать все сессии. Но я его не делал. Вот почему.


Почему закрыл

После 5 дней Ghost показывал: «Warp 4ч, Telegram 1ч, пик в 13-15ч». Это Screen Time, который бесплатный и встроен в macOS.

Паттерн «ты в 22:00 открываешь Telegram → Perplexity» - ну, я и так это знаю. Автоматизация - «открыть Perplexity когда ты открыл Telegram в 22:00». Кто за это заплатит $7/мес?

Я скормил Ghost двум AI-моделям с просьбой оценить как VC при due diligence. Оба вернули одинаковое: технология есть, продукта нет.

Три аргумента, с которыми сложно спорить.

Ghost продаёт наблюдение, а не результат. Пользователь покупает исчезающую боль. «Он замечает мои паттерны» - не боль. «Он иногда открывает приложения за меня» - не боль. «Он предупреждает, что я залип» - для большинства тоже не боль.

Отложенная ценность. Продукт требует дней наблюдения, чтобы показать хоть что-то. Поставил сегодня, а «aha» обещан через неделю. Большинство утилит умирает именно здесь.

Рынок не работает. Rewind.ai привлёк $33M от a16z, пивотнулся из софта в hardware-кулон за $99, в декабре 2025 куплен Meta. Mac-приложение отключено, данные удалены. Humane AI Pin - $699 + подписка, возвраты превышали продажи, HP купила активы за $116M (при запросе $750M). RescueTime - 15+ лет, $2.6M выручки в 2024, стабильная ниша. Ни один ambient observer не стал большим бизнесом.

И самый сильный сигнал. Я сам не пользовался продуктом. За 5 дней ни разу не открыл popup и не подумал «о, полезно». Для ambient utility это приговор. Если основатель не открывает своё приложение - пользователи и подавно не будут.

Оба AI предложили один pivot

Автоматический тайм-трекинг для фрилансеров. Ghost тихо записывает сессии, группирует через LLM по проектам (используя window titles), в пятницу выдаёт готовый таймшит. Боль реальная - фрилансеры теряют 2-3 часа в неделю, реконструируя рабочий день по памяти.

Направление рабочее. Но я не фрилансер, не биллю клиентов по часам и не заполняю таймшиты. Строить продукт для чужой боли можно - но это другой режим, с customer development, интервью и итерациями. Не мой случай.


Кладбище ambient AI

Ambient AI / desktop observer в 2024-2026:

ПроектПривлёкЧем кончилось
Rewind / Limitless$33M (a16z)Pivot в hardware → куплен Meta (дек 2025)
Humane AI Pin$230M+Провал ($699 + подписка), куплен HP за $116M (фев 2025)
Microsoft Recall-Privacy-скандал, отложен, выпущен opt-in
RescueTime-15+ лет, ~$2.6M/yr (2024), плато
Timing-Нишевый, $108-192/yr, стабильно

Общий паттерн: пользователи не доверяют приложениям, которые «записывают всё». Microsoft получил privacy backlash за Recall - данные хранились в незашифрованной SQLite, доступной любому процессу. Rewind обещал локальность, но PMF не нашёл и ушёл в hardware.

Выжили узкоспециализированные инструменты с конкретной задачей (тайм-трекинг), а не те, кто пытался «наблюдать за всем».


Что забрать из проекта

Ghost как продукт не нашёл боль. Как инженерный эксперимент - оставил три вещи, которые пригодятся в других проектах.

Tauri 2.0 + Swift daemon

В open source почти нет примеров связки Tauri с нативным Swift-процессом. Обычно Tauri-приложения - веб-обёртки. Здесь другой паттерн: отдельный Swift daemon для системных API, WebSocket-мост, Rust backend для логики и хранения. Это переносится на любое macOS-приложение с Accessibility API, Screen Capture или другими привилегированными API.

Local LLM pipeline

Ollama + structured JSON output + confidence scoring + trust model (max 1 suggestion/day). Такая обвязка работает для чего угодно, где нужен LLM без облака. Промпт → JSON → валидация → очередь решений → действие.

Privacy-first хранение

SQLite + WAL + 30-day rolling cleanup + «Delete All Data» с VACUUM. Готовый шаблон для приложений с чувствительными данными и полным контролем пользователя.


Код

Проект под MIT: github.com/spyrae/ambientghost

Tauri 2.0, Rust, Swift, React 19, SQLite, Ollama. macOS 13+.

Если строите macOS-приложение с нативными API, local LLM или ambient-наблюдением - форкайте, адаптируйте, используйте как стартовую точку.