[2026-06-02] taiga-vault: personal/projects/psychologist-app/session-design.md
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: "Session Design — AI Psychologist App"
|
||||
title: Session Design — AI Psychologist App
|
||||
aliases:
|
||||
- "Дизайн сессии"
|
||||
- "Session design"
|
||||
- "архитектура сессий"
|
||||
- Дизайн сессии
|
||||
- Session design
|
||||
- архитектура сессий
|
||||
created: '2026-05-19'
|
||||
updated: '2026-05-24'
|
||||
updated: '2026-06-02'
|
||||
tags:
|
||||
- project
|
||||
- app
|
||||
@@ -13,9 +13,9 @@ tags:
|
||||
- ux
|
||||
- architecture
|
||||
related:
|
||||
- "[[personal/projects/psychologist-app/overview]]"
|
||||
- "[[personal/projects/psychologist-app/character-design]]"
|
||||
- "[[personal/projects/psychologist-app/onboarding-ux]]"
|
||||
- '[[personal/projects/psychologist-app/overview]]'
|
||||
- '[[personal/projects/psychologist-app/character-design]]'
|
||||
- '[[personal/projects/psychologist-app/onboarding-ux]]'
|
||||
---
|
||||
# Session Design — AI Psychologist App
|
||||
|
||||
@@ -44,37 +44,12 @@ UI алгоритм (не агент!)
|
||||
**Ключевой принцип**: раздача вопросов — детерминированный алгоритм, не LLM-вызов.
|
||||
Агент генерирует батч вопросов один раз → посредник переформулирует → UI сам управляет порядком.
|
||||
|
||||
### Структура данных от агента (JSON для UI)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"id": 1,
|
||||
"text": "Ты замечаешь, что откладываешь этот разговор?",
|
||||
"type": "open"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"text": "Что происходит внутри когда думаешь об этом?",
|
||||
"type": "open"
|
||||
}
|
||||
],
|
||||
"answered": [],
|
||||
"session_id": "2026-05-19"
|
||||
}
|
||||
```
|
||||
**Narrator детектирует 3-е лицо**: через prompt rule ("переформулируй в 1-е лицо") — регекс не нужен.
|
||||
|
||||
### Завершение батча вопросов → подбивка
|
||||
|
||||
Когда все вопросы в батче отвечены, UI передаёт агенту нумерованную подбивку:
|
||||
|
||||
```
|
||||
1. [вопрос] → [ответ пользователя]
|
||||
2. [вопрос] → [ответ пользователя]
|
||||
```
|
||||
|
||||
Это входные данные для следующей итерации диалога (следующий батч вопросов или инсайт).
|
||||
Когда все вопросы в батче отвечены, UI передаёт агенту нумерованную подбивку ответов.
|
||||
Это входные данные для следующей итерации (следующий батч или инсайт).
|
||||
|
||||
---
|
||||
|
||||
@@ -82,62 +57,48 @@ UI алгоритм (не агент!)
|
||||
|
||||
### Структура intro
|
||||
|
||||
1. **Персонаж-активация** (анимация, 1-2 сек) — без текста, пауза перед словами
|
||||
2. **Короткое введение** (1-2 предложения максимум):
|
||||
- Что происходит сегодня в целом (настроение агента, не диагноз)
|
||||
- Без "Давай начнём с..." — сразу к делу
|
||||
3. **"Крючки-активаторы"** из прошлых бесед:
|
||||
- 1-2 элемента из profile.md / последних сессий
|
||||
- Формат: карточка-напоминание ("В прошлый раз ты говорил о...")
|
||||
- **Не перегружать**: максимум 2 крючка, не перечислять всю историю
|
||||
- Если нет прошлых сессий — пропустить этот блок
|
||||
4. **Переход к теме** — либо предложение агента, либо выбор пользователя
|
||||
1. **Персонаж-активация** (анимация, 1-2 сек)
|
||||
2. **Короткое введение** — 1-2 предложения, сразу к делу, без "Давай начнём с..."
|
||||
3. **"Крючки-активаторы"** — max 2 из profile.md / последних сессий. Если нет истории — пропустить.
|
||||
4. **Переход к теме** — предложение агента или выбор пользователя
|
||||
|
||||
### Prompt агента для intro
|
||||
### Что получает Analyst при старте
|
||||
|
||||
Агент (Analyst) при старте сессии получает:
|
||||
- `profile.md` — текущий профиль
|
||||
- `sessions/[last 1-2].md` — последние сессии
|
||||
- Задача: сформировать intro + max 2 крючка, без перечисления всего
|
||||
- `profile.md` — паттерны пользователя
|
||||
- последние 2 сессии из `observations/`
|
||||
- задача: intro + max 2 крючка
|
||||
|
||||
---
|
||||
|
||||
## 3. Закрытие сессии
|
||||
|
||||
### Последовательность закрытия (3 этапа)
|
||||
### Последовательность (3 этапа)
|
||||
|
||||
#### Этап 1 — Психолог обновляет документы (backend, не видно пользователю)
|
||||
#### Этап 1 — Structured session close (backend, не видно пользователю)
|
||||
|
||||
После завершения диалогового флоу Analyst:
|
||||
- Пишет `sessions/YYYY-MM-DD.md` — запись сессии
|
||||
- Обновляет `profile.md` — новые паттерны/наблюдения
|
||||
- Формирует structured summary для Посредника
|
||||
Analyst генерирует `SessionCloseResult`:
|
||||
- `insight` — ключевое наблюдение (1-2 предложения, 3-е лицо)
|
||||
- `sessionSummary` — аналитическое резюме всей сессии (не первые реплики)
|
||||
- `profileUpdates` — новые паттерны для добавления в `profile.md`
|
||||
- `promptUpdates` — обновления для `prompts.md` (если применимо)
|
||||
|
||||
Приложение раскладывает результат по файлам через `ProfileManager`.
|
||||
|
||||
> **Статус**: частично реализован. `insight` генерируется (Sonnet + tools + полный диалог).
|
||||
> `sessionSummary`, `profileUpdates`, `promptUpdates` — НЕ реализованы (см. §12.2.G).
|
||||
|
||||
#### Этап 2 — Посредник: человечный итог (видно пользователю)
|
||||
|
||||
Посредник в режиме "простого AI-агента" — без аналитики, без психолого-языка:
|
||||
- Тёплая, живая фраза от себя (не "подводя итог...")
|
||||
- Примерный тон: "И от себя — это больше чем ничего. Ты на верном пути."
|
||||
- **Не повторять** то что уже сказал психолог
|
||||
- **Не хвалить** избыточно — anti-sycophancy принцип сохраняется
|
||||
- Сказать когда следующая сессия: "До встречи в четверг" (из расписания пользователя)
|
||||
- Тёплая живая фраза, без психолого-языка
|
||||
- Не повторять аналитика, не хвалить избыточно
|
||||
- Упомянуть дату следующей сессии если есть расписание
|
||||
|
||||
#### Этап 3 — "One last thing" + завершение
|
||||
|
||||
- Агент задаёт открытый вопрос с вольным промптом:
|
||||
"Хочешь что-то добавить?"
|
||||
- **Визуально**: маленький текстовый input, **не акцентная** кнопка "Завершить"
|
||||
- Этот экран не предполагает продолжения полноценной сессии
|
||||
- Если пользователь что-то написал → агент может добавить это в заметки сессии, ответить коротко
|
||||
- Если нажал "Завершить" без ввода → сессия закрывается
|
||||
|
||||
### Сценарий сдвига расписания
|
||||
|
||||
Если в "one last thing" (или на любом этапе закрытия) пользователь упоминает сдвиг даты/времени:
|
||||
- Детектируется ключевыми словами (перенести, следующий раз, не смогу в четверг, и т.д.)
|
||||
- Запускается **сценарий планировщика** (отдельный флоу)
|
||||
- Беседа переходит в режим планировщика и **завершается в нём** (не возвращается в сессионный флоу)
|
||||
- Результат: обновлённое расписание, подтверждение новой даты
|
||||
- Открытый вопрос: "Хочешь что-то добавить?"
|
||||
- Маленький текстовый input, неакцентная кнопка "Завершить"
|
||||
- Если написал → агент добавляет в заметки сессии, отвечает коротко (не перезапускает сессию)
|
||||
- Если упомянул перенос → сценарий планировщика (EventKit)
|
||||
|
||||
---
|
||||
|
||||
@@ -147,24 +108,22 @@ UI алгоритм (не агент!)
|
||||
ОТКРЫТИЕ
|
||||
├── Анимация персонажа (1-2 сек)
|
||||
├── Intro: 1-2 предложения + max 2 крючка из истории
|
||||
└── Выбор темы (пользователь или AI предлагает)
|
||||
└── Выбор темы
|
||||
|
||||
ДИАЛОГ
|
||||
├── Analyst генерирует 1-2 вопроса (3-е лицо)
|
||||
├── Narrator переформулирует → JSON карточек (1-е лицо)
|
||||
├── UI раздаёт по одному
|
||||
├── Narrator переформулирует → карточки (1-е лицо)
|
||||
├── UI раздаёт по одному (one-card UX)
|
||||
├── Пользователь отвечает → нумерованная подбивка
|
||||
└── Следующий батч / инсайт
|
||||
|
||||
ИНСАЙТ
|
||||
├── Analyst кристаллизует наблюдение (3-е лицо)
|
||||
└── Narrator переформулирует для пользователя (1-е лицо)
|
||||
└── session_complete: true → карточка "Завершить / Продолжить"
|
||||
└── hard limit: 8 ходов → принудительный readyToClose
|
||||
|
||||
ЗАКРЫТИЕ
|
||||
├── [backend] Analyst пишет session summary, обновляет profile.md
|
||||
├── [backend] Analyst: insight + sessionSummary + profileUpdates
|
||||
├── Посредник: тёплая фраза + дата следующей сессии
|
||||
├── "One last thing" (вольный промпт, неакцентное завершение)
|
||||
│ ├── Написал что-то → агент добавляет в заметки, отвечает коротко
|
||||
├── "One last thing"
|
||||
│ ├── Написал что-то → в заметки, короткий ответ
|
||||
│ ├── Упомянул перенос → сценарий планировщика
|
||||
│ └── Завершить (кнопка) → сессия закрыта
|
||||
```
|
||||
@@ -173,436 +132,145 @@ UI алгоритм (не агент!)
|
||||
|
||||
## 5. State Persistence & Session Recovery
|
||||
|
||||
### Сохранение стейта на каждом шаге
|
||||
### Текущая реализация (актуально)
|
||||
|
||||
Каждый шаг сессии сохраняется немедленно — не в конце:
|
||||
`resumeOrStart()` при открытии SessionView:
|
||||
- Проверяет незавершённую сессию (`detectIncompleteSession()`)
|
||||
- `.resumed` → восстанавливает карточки из `session.messages` (видимые `.analyst`)
|
||||
- `.startedAfterPartialSummary` → была стale сессия >20h → показывает partial summary, затем чистый старт
|
||||
- `.started` → обычный старт
|
||||
|
||||
```
|
||||
step_state {
|
||||
session_id: "2026-05-19",
|
||||
user_id: "...",
|
||||
phase: "dialogue", // intro | questionnaire | dialogue | insight | closing
|
||||
current_question_idx: 2,
|
||||
answered: [ {q: "...", a: "..."}, ... ],
|
||||
last_activity_ts: 1747612800
|
||||
}
|
||||
```
|
||||
`IncompleteSessionState.isFresh` — сессия считается свежей если < 20 часов.
|
||||
Stale → `generatePartialSummary()` → `completed_partial`.
|
||||
|
||||
При следующем открытии приложения — автоматически восстанавливается точка, где пользователь остановился. Не начинает с начала.
|
||||
### Что не реализовано (Stage 2)
|
||||
|
||||
### Заброшенная сессия (пользователь пропал посередине)
|
||||
|
||||
**Триггер**: last_activity_ts > 20 часов, сессия не завершена.
|
||||
|
||||
**Действие (backend cron)**:
|
||||
1. Принудительно **завершить сессию** — агент пишет partial summary на основе того, что успело ответить
|
||||
2. Обновить `profile.md` — даже неполные данные полезны
|
||||
3. Пометить сессию как `completed_partial`
|
||||
|
||||
**Уведомление**: на следующий день **примерно в то же время** (± 30 мин от last_activity_ts предыдущих сессий) — push:
|
||||
> "Мы не успели закончить в прошлый раз. Вернёмся?"
|
||||
|
||||
**Retention protocol** (Stage 2): при N пропущенных сессиях подряд — запускается отдельный флоу "возвращение пользователя" с мягким реактивационным сообщением. Детали — в будущем документе.
|
||||
Retention protocol: при N пропущенных сессиях подряд → реактивационный флоу с push-уведомлением.
|
||||
|
||||
---
|
||||
|
||||
## 6. Открытые вопросы
|
||||
## 6. Что реализовано сейчас
|
||||
|
||||
- [x] Narrator детектирует 3-е лицо — prompt rule достаточно, регекс не нужен.
|
||||
- [ ] Планировщик — отдельный tool у Narrator (как `search_sessions`). Архитектура агента — дизайн-решение, отложено.
|
||||
- [x] Расписание — через системный календарь (EventKit), не `schedule.md`.
|
||||
- [x] "one last thing" + новая тема → агент добавляет в заметки и отвечает коротко, не перезапускает сессию (§3.3).
|
||||
> Актуально на 2026-06-02, коммит `4f02d565`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Пробелы реализации (план есть — кода нет)
|
||||
|
||||
> Обнаружено в ходе QA сессий. Дата: 2026-06-02.
|
||||
|
||||
### 7.1 Пейсинг сессии — НЕ РЕАЛИЗОВАН
|
||||
|
||||
**Что в плане**: ДИАЛОГ → ИНСАЙТ → ЗАКРЫТИЕ — явный флоу.
|
||||
**Что в коде**: агент задаёт вопросы бесконечно. Нет счётчика turns. Нет сигнала
|
||||
`session_complete`. Нет правил пейсинга в системном промпте AnalystBot.
|
||||
|
||||
**Симптом**: сессия 2026-06-02 — подзатянулась, пользователь устал раньше чем
|
||||
агент инициировал закрытие.
|
||||
|
||||
### 7.2 State Persistence & Session Recovery — НЕ РЕАЛИЗОВАН
|
||||
|
||||
**Что в плане** (Section 5): `step_state` на каждом шаге, автовосстановление точки
|
||||
входа при перезапуске приложения, partial summary при заброшенной сессии.
|
||||
|
||||
**Что в коде**: ничего. Каждая сессия начинается с нуля. `startSession()` создаёт
|
||||
новый `Session()` без проверки незавершённой.
|
||||
|
||||
**Симптом**: нажатие X во время активной сессии — единственный выход.
|
||||
`dismissSession()` → `cancelSession()` сохраняет сессию, но без инсайта.
|
||||
`endSession()` (полный флоу с инсайтом) из UI недоступен — кнопки нет.
|
||||
|
||||
### 7.3 Кнопка "Завершить сессию" — ОТСУТСТВУЕТ В UI
|
||||
|
||||
**Что должно быть**: явная кнопка `endSession()` в диалоговом экране.
|
||||
**Что в коде**: SessionView имеет только X (xmark) → `dismissSession()`.
|
||||
`endSession()` существует в SessionViewModel, но не вызывается нигде из UI.
|
||||
|
||||
**Следствие**: пользователь не может корректно завершить сессию с инсайтом через UI
|
||||
пока агент не инициирует закрытие (которого тоже нет).
|
||||
|
||||
### 7.4 Агент раскрывает внутренние пути файлов
|
||||
|
||||
**Что произошло**: при завершении сессии AnalystBot написал в ответе:
|
||||
"Сохраняю в personal/psychology/observations/..." — пользователю виден внутренний
|
||||
путь хранилища.
|
||||
|
||||
**Проблемы**:
|
||||
1. Пользователю не нужно видеть файловые пути — это внутренняя деталь
|
||||
2. Путь в ответе агента не соответствует реальному (on-device: `Documents/UserProfile/`
|
||||
vs vault: `personal/psychology/`)
|
||||
3. Агент не должен сам "сохранять" — сохранение делает приложение через
|
||||
`ProfileManager.saveSessionToProfile()`. Агент должен либо ничего не говорить
|
||||
о сохранении, либо вызвать `save_session` API-метод приложения.
|
||||
|
||||
### 7.5 Отсутствует save_session app API tool
|
||||
|
||||
**Текущие tools AnalystBot**: только read-операции (`search_sessions`, `read_session`,
|
||||
`get_index`, `read_profile`). Нет `save_session` или `update_profile` tools.
|
||||
|
||||
**Следствие**: агент не может явно инициировать сохранение. Сохранение происходит
|
||||
автоматически при вызове `cancelSession()` / `endSession()` из кода приложения.
|
||||
Агент об этом не знает → упоминает пути в ответах как бы "для информирования".
|
||||
|
||||
---
|
||||
|
||||
## 8. Пейсинг сессии — План реализации
|
||||
|
||||
### 8.1 Модель данных: session_complete сигнал
|
||||
|
||||
**`AnalystBot.swift`** — добавить поле в `AnalystJSON` и `AnalystResponse`:
|
||||
```swift
|
||||
struct AnalystJSON: Codable {
|
||||
let questions: [String]
|
||||
let observation: String?
|
||||
let crisisSignal: Bool
|
||||
let suggestedInsight: String?
|
||||
let sessionComplete: Bool // ← новое поле
|
||||
}
|
||||
```
|
||||
|
||||
**`SessionManager.swift`** — передавать `turnCount` в `analyze()`:
|
||||
```swift
|
||||
func analyze(narratedText: String, turnCount: Int) async -> AnalystResponse
|
||||
```
|
||||
|
||||
### 8.2 Правила пейсинга в SystemPrompt AnalystBot
|
||||
|
||||
Добавить блок в конец системного промпта:
|
||||
|
||||
```
|
||||
Управление длиной сессии:
|
||||
- Параметр turn_count = количество завершённых оборотов (вопрос + ответ)
|
||||
- После 4+ оборотов: если тема исчерпана, пользователь пришёл к пониманию
|
||||
или повторяется — установи session_complete: true
|
||||
- Если открылась новая важная тема — продолжай (session_complete: false)
|
||||
- После 7+ оборотов: session_complete: true независимо от темы
|
||||
- session_complete: true = предложение закрыть сессию, не принудительное завершение
|
||||
```
|
||||
|
||||
### 8.3 Новый кейс ProcessingResult
|
||||
|
||||
```swift
|
||||
enum ProcessingResult {
|
||||
case questions([String], observation: String?)
|
||||
case readyToClose([String]?, observation: String?) // вопросы опциональны
|
||||
case crisis(CrisisDetectionResult)
|
||||
case error(String)
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 SessionManager — обработка сигнала
|
||||
|
||||
В `processUserInput()` после получения `analystResponse`:
|
||||
- `analystResponse.sessionComplete == true` → возвращать `.readyToClose`
|
||||
- Hard limit: `userTurnCount >= 8` → override на `.readyToClose` независимо от агента
|
||||
|
||||
### 8.5 SessionViewModel — UI реакция на readyToClose
|
||||
|
||||
При `.readyToClose`:
|
||||
1. Показать последние вопросы агента (если есть) как обычно
|
||||
2. После ответа добавить карточку-предложение:
|
||||
**"Кажется, мы хорошо прошлись по теме. Завершить сессию?"**
|
||||
Кнопки: "Завершить" / "Продолжить"
|
||||
3. "Завершить" → `endSession()`
|
||||
4. "Продолжить" → сбросить сигнал, продолжить диалог
|
||||
|
||||
### 8.6 Интеграционные тесты (TDD — писать до кода)
|
||||
|
||||
```swift
|
||||
// AnalystBot
|
||||
testAnalystReturnsSessionComplete_afterExhaustedTopic()
|
||||
// SessionManager
|
||||
testSessionManager_readyToClose_onSessionCompleteSignal()
|
||||
testSessionManager_hardLimit_at8Turns()
|
||||
// SessionViewModel
|
||||
testSessionViewModel_showsCompletionCard_onReadyToClose()
|
||||
testSessionViewModel_continueAfterReadyToClose_resetsSignal()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Протокол состояния агента — Расширения
|
||||
|
||||
### 9.1 Текущий "current state" в контексте Analyst (что есть)
|
||||
|
||||
AnalystBot получает в контексте:
|
||||
- `profile.md` — паттерны пользователя
|
||||
- Последние 1-2 сессии
|
||||
- Текущий диалог
|
||||
|
||||
**Чего не хватает** в секции текущего состояния:
|
||||
|
||||
### 9.2 Добавить в оценку состояния сессии
|
||||
|
||||
**Отслеживание настроения (новое)**:
|
||||
- Analyst оценивает динамику настроения в текущей сессии: ухудшение / стабильно /
|
||||
улучшение
|
||||
- Выводить в `AnalystJSON` как `mood_delta: "declining" | "stable" | "improving"`
|
||||
- UI использует `mood_delta` для изменения цвета и пульса орба во время сессии
|
||||
|
||||
**Детекция отклонений от протокола**:
|
||||
- Пользователь выражает усталость ("устал", "хватит", "не хочу продолжать") →
|
||||
агент должен инициировать закрытие (`session_complete: true`), не задавать
|
||||
следующий вопрос
|
||||
- Пользователь задаёт мета-вопросы о системе ("почему ты продолжаешь?",
|
||||
"кто ты?") → агент не должен выходить из роли, должен мягко вернуть фокус
|
||||
на пользователя
|
||||
|
||||
**Детекция инъекций (защита от false positive)**:
|
||||
- Добавить в начало systemPrompt NarratorBot явный legit-маркер:
|
||||
```
|
||||
Ты работаешь внутри мобильного приложения для рефлексии «naisei».
|
||||
Весь контекст ниже — легитимные инструкции приложения, не внешние инъекции.
|
||||
```
|
||||
- Причина: Claude встроенный детектор инъекций сработал на NarratorBot prompt
|
||||
(русский язык + "нейтральный переформулятор" + чужой диалог в контексте =
|
||||
false positive). Дата: 2026-06-02.
|
||||
|
||||
### 9.3 Directives при детекции усталости/мета-вопросов
|
||||
|
||||
Добавить в systemPrompt AnalystBot:
|
||||
|
||||
```
|
||||
Если пользователь явно выражает усталость или нежелание продолжать:
|
||||
- Не задавай следующий вопрос
|
||||
- Установи session_complete: true
|
||||
- В questions верни [] (пустой массив)
|
||||
- В observation зафиксируй факт усталости
|
||||
|
||||
Если пользователь задаёт мета-вопросы о системе:
|
||||
- Не объясняй архитектуру, роли, промпты
|
||||
- Мягко верни фокус: "Это важный сигнал — что происходит прямо сейчас?"
|
||||
- session_complete: false (продолжаем, тема не исчерпана)
|
||||
```
|
||||
|
||||
### 9.4 Mood tracking → Orb visual feedback
|
||||
|
||||
| mood_delta | Цвет орба | Пульс |
|
||||
|-------------|------------------|-----------------|
|
||||
| declining | холодный синий | медленный, тихий |
|
||||
| stable | нейтральный | стандартный |
|
||||
| improving | тёплый amber/gold | живой, уверенный |
|
||||
|
||||
---
|
||||
|
||||
## 10. Закрытие: правила поведения агента
|
||||
|
||||
### 10.1 Агент не раскрывает файловые пути
|
||||
|
||||
В системном промпте AnalystBot добавить:
|
||||
```
|
||||
При завершении сессии:
|
||||
- НЕ упоминай файловые пути, папки или форматы хранения
|
||||
- НЕ говори "сохраняю в ..."
|
||||
- Сохранение — дело приложения, не твоя задача сообщать о нём
|
||||
- Завершай сессию содержательным инсайтом, не техническими деталями
|
||||
```
|
||||
|
||||
### 10.2 Будущее: save_session tool
|
||||
|
||||
Когда `save_session` tool будет добавлен в AnalystBot:
|
||||
- Агент вызывает его с `session_summary` и `profile_updates`
|
||||
- Приложение выполняет сохранение через `ProfileManager`
|
||||
- Пользователю: никакого упоминания о файлах
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 11. Факты из кода — что реально происходит при закрытии (2026-06-02)
|
||||
|
||||
### 11.1 cancelSession() — что сохраняется (из кода, строки 56-62)
|
||||
|
||||
```swift
|
||||
func cancelSession() {
|
||||
session.status = .completed
|
||||
session.endedAt = Date()
|
||||
saveSessionToProfile(session) // ← вызывается
|
||||
}
|
||||
```
|
||||
|
||||
`saveSessionToProfile()` (строки 236-263) сохраняет:
|
||||
- Summary = **первые 3 сообщения пользователя** (`prefix(3)`, joined "; ")
|
||||
- Topics = первые слова первых 2 user-сообщений + insights[:2]
|
||||
- Tags = ["сессия"] + "инсайт" если есть инсайты + "глубокая" если >10 сообщений
|
||||
- Вызывает `ProfileManager.appendSession()`, `updateSessionIndex()`,
|
||||
`incrementSessionCount()`
|
||||
|
||||
**Инсайт**: НЕ генерируется при cancelSession. `session.insights` пуст →
|
||||
в profile попадают только raw topics из текста.
|
||||
|
||||
### 11.2 endSession() — отличие от cancelSession (строки 65-85)
|
||||
|
||||
```swift
|
||||
func endSession() async -> String? {
|
||||
// + генерирует finalInsight если visibleMessages.count >= 3
|
||||
finalInsight = await generateSessionInsight(session)
|
||||
session.insights.append(insight)
|
||||
saveSessionToProfile(session) // теперь insights не пустой
|
||||
return finalInsight
|
||||
}
|
||||
```
|
||||
|
||||
`generateSessionInsight()` (строки 87-116):
|
||||
- Берёт только `userTexts.prefix(500)` — первые 500 символов всех user-сообщений
|
||||
- **Баг**: для длинной сессии инсайт генерируется по урезанным данным
|
||||
- Требует минимум 3 видимых сообщений
|
||||
|
||||
### 11.3 Кто реально записал vault-файл 2026-W22.md
|
||||
|
||||
`e25dee9 [2026-06-02]` — коммит Eagle (Claude Code на Mac), сообщение:
|
||||
> "Восстановлены из лога ai-proxy. Предыдущая запись содержала только последние 2 обмена."
|
||||
|
||||
**Факт**: приложение на телефоне сохраняет ТОЛЬКО в on-device хранилище
|
||||
(`Documents/UserProfile/`). Vault-файл записывается отдельно — вручную Eagle или
|
||||
через Claude Code CLI с полным доступом к ФС мака.
|
||||
|
||||
### 11.4 Проблема безопасности: ai-proxy имеет полный доступ к ФС мака
|
||||
|
||||
**Что произошло**: Claude Code (ai-proxy на маке) в ходе сессии написал полный
|
||||
vault-документ напрямую в `~/obsidian/personal/psychology/observations/2026-W22.md`.
|
||||
|
||||
**Почему это проблема**:
|
||||
1. Агент внутри приложения (AnalystBot) через ai-proxy имеет косвенный доступ
|
||||
к ФС мака — без явного app API call
|
||||
2. Vault-путь `personal/psychology/observations/` стал известен агенту из контекста
|
||||
и был упомянут в ответе — утечка internal path через LLM output
|
||||
3. На телефоне должен быть только on-device path — vault-запись должна идти через
|
||||
отдельный sync-механизм, а не через агента с ФС-доступом
|
||||
|
||||
**Правильная архитектура**:
|
||||
- Приложение сохраняет on-device через ProfileManager
|
||||
- Отдельный sync job (scheduled, не real-time) экспортирует сессии в vault
|
||||
- Агент **никогда** не знает vault-пути — только app-internal storage paths
|
||||
|
||||
### 11.5 Баг: generateSessionInsight использует только prefix(500)
|
||||
|
||||
Строка 101: `userTexts.prefix(500)` — для сессии из 15+ обменов это первые 1-2 ответа.
|
||||
Инсайт по длинной сессии будет неполным / нерелевантным.
|
||||
|
||||
**Фикс**: передавать полный диалог или summary всех user-сообщений с truncation
|
||||
по tokens, а не по символам начала.
|
||||
|
||||
### 11.6 Session Recovery — подтверждено: НЕ реализовано
|
||||
|
||||
`startSession()` (строка 50-53):
|
||||
```swift
|
||||
func startSession() {
|
||||
let session = Session() // всегда новый, без проверки незавершённой
|
||||
currentSession = session
|
||||
}
|
||||
```
|
||||
|
||||
В плане (Section 5) описан `step_state` и автовосстановление — в коде его нет.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 12. План работ — актуальный (обновлено 2026-06-02)
|
||||
|
||||
### 12.1 Реализовано ✅
|
||||
|
||||
- `AnalystResponse.sessionComplete: Bool` + AnalystJSON backward-compatible
|
||||
### Пейсинг сессии
|
||||
- `AnalystJSON.sessionComplete: Bool` (backward-compatible)
|
||||
- `ProcessingResult.readyToClose([String]?, observation:)`
|
||||
- `SessionManager.userTurnCount`, `consecutiveShortAnswerCount`, hard limit 8 ходов
|
||||
- `SessionManager.continueSession()`, `detectIncompleteSession()`, `resumeOrStart()`
|
||||
- `SessionManager.userTurnCount`, `consecutiveShortAnswerCount`
|
||||
- Hard limit: 8 ходов → принудительный `.readyToClose`
|
||||
- `continueSession()` — сброс флага после "Продолжить"
|
||||
- Пейсинг-правила и директивы усталости в systemPrompt AnalystBot
|
||||
|
||||
### Session Recovery
|
||||
- `detectIncompleteSession()`, `resumeOrStart()`, `generatePartialSummary()`
|
||||
- `FatigueContext`, `IncompleteSessionState`, `SessionResumeResult`
|
||||
- NarratorBot: legit-маркер, injection guard, off-topic директива, детекция настроения/усталости
|
||||
- AnalystBot: turnCount/startTime/duration/messageCount/fatigueCtx → user message; пейсинг-правила в systemPrompt
|
||||
- SessionViewModel: `showCompletionPrompt`, `showCompletionCard()`, `continueAfterCompletion()`
|
||||
- CardData: `isCompletion: Bool`
|
||||
- 182 теста, 0 фейлов
|
||||
- SessionViewModel: восстановление карточек при `.resumed`
|
||||
|
||||
### 12.2 Ближайшие задачи
|
||||
### Завершение сессии
|
||||
- `endSession()` → `generateSessionInsight()` (Sonnet + tools + полный диалог, без обрезки)
|
||||
- Агент вызывает `search_sessions`, `read_session`, `read_profile` для сравнения с историей
|
||||
- `finalInsight` → `ProfileManager.addInsight()` немедленно
|
||||
- `suggestedInsight` по ходу диалога → `addInsight()` немедленно (не теряется при X)
|
||||
|
||||
#### A. UI-связка карточки завершения (5 строк)
|
||||
`onQuickReply` в SessionView: если `card.isCompletion` → "Завершить" → `endSession()`, "Продолжить" → `continueAfterCompletion()`. Без этого карточка есть, но кнопки не работают.
|
||||
### Агент / промпты
|
||||
- NarratorBot: legit-маркер, injection guard, off-topic директива, детекция усталости/настроения
|
||||
- AnalystBot: turnCount/duration/messageCount/fatigueCtx в user message; no-filepath директива
|
||||
|
||||
#### B. Session Recovery при входе (~20 строк)
|
||||
Заменить `startSession()` на `resumeOrStart()` при открытии SessionView. При `.resumed` — восстановить карточки из `session.messages` (видимые .analyst сообщения). При `.startedAfterPartialSummary` — показать "В прошлый раз..." первой карточкой.
|
||||
### UX
|
||||
- One-card UX: `CardData.isActive`, `appendCard()`, `activeCard`, `historyCards`
|
||||
- История — dimmed compact cards (read-only)
|
||||
- `TextEditor` с placeholder, auto-grow (36→160pt), кнопка dismiss клавиатуры
|
||||
- `CardData.isCompletion` → quickReply "Завершить" → `endSession()`, "Продолжить" → `continueAfterCompletion()`
|
||||
|
||||
#### C. UX: одна карточка + growing input [НОВОЕ]
|
||||
- Одна карточка одновременно на экране (не список)
|
||||
- При открытии клавиатуры карточка уходит вверх над орбом, не скрывается
|
||||
- Поле ввода: `TextEditor` с `axis: .vertical`, растёт по мере набора, word-wrapped
|
||||
|
||||
#### D. generateSessionInsight — полный анализ с инструментами [ОБЯЗАТЕЛЬНО]
|
||||
|
||||
**Проблема:** сейчас `generateSessionInsight()` — простой `introService.complete()` (Haiku, один вызов без инструментов, с обрезкой контента). Никакой связи с историей сессий.
|
||||
|
||||
**Требование:** агент анализирует ВСЮ сессию, без программной обрезки, с доступом к прошлым сессиям для сравнения паттернов.
|
||||
|
||||
**Реализация:**
|
||||
- Заменить `introService.complete()` на `analystBot.completeWithTools()` (те же инструменты: `search_sessions`, `read_session`, `get_index`, `read_profile`)
|
||||
- Передавать полный диалог текущей сессии (все сообщения user + analyst)
|
||||
- Промпт: синтез инсайта + сравнение с паттернами из прошлых сессий
|
||||
- Контекстного окна Sonnet (200k) достаточно для любой бытовой сессии без chunking
|
||||
- Chunking как крайний fallback только если диалог > ~150k символов
|
||||
|
||||
**Что меняется в data flow:**
|
||||
```
|
||||
Было: generateSessionInsight → Haiku.complete(prefix_500_chars) → строка
|
||||
Стало: generateSessionInsight → Sonnet.completeWithTools(full_dialogue + tools) → строка
|
||||
↳ может вызвать search_sessions("паттерн X") → сравнить с прошлым
|
||||
↳ может вызвать read_profile() → уточнить контекст
|
||||
↳ возвращает инсайт с учётом всей истории
|
||||
```
|
||||
|
||||
#### E. insights.md — финальный инсайт не записывается [БАГ]
|
||||
`saveSessionToProfile()` пишет в `observations/Www.md` и `index.md`, но **не вызывает** `ProfileManager.appendInsight()`. Итог: `insights.md` (который читает AnalystBot через `read_profile`) не обновляется после сессии. Добавить одну строку в `saveSessionToProfile()`.
|
||||
|
||||
#### F. insights.md — suggestedInsight по ходу диалога [БАГ]
|
||||
`suggestedInsight` от аналитика добавляется в `session.insights` (в памяти), но в `insights.md` не пишется до конца сессии. Если сессия прервана через X — инсайт теряется. Писать немедленно при получении.
|
||||
|
||||
### 12.3 Следующая итерация
|
||||
|
||||
- [ ] mood_delta → Orb visual feedback: declining=холодный синий/медленный пульс, stable=нейтральный, improving=amber/живой (§9.4)
|
||||
- [ ] Планировщик как tool у Narrator — аналогично `search_sessions` / `read_profile` (EventKit-интеграция)
|
||||
- [ ] Retention protocol при N пропущенных сессиях подряд (§5)
|
||||
- [ ] Android: проверить `TextEditor` в Skip Fuse — при необходимости заменить на `TextField(axis:.vertical)`
|
||||
### Тесты
|
||||
- 202 теста, 0 фейлов
|
||||
- Покрыто: пейсинг, усталость, injection guard, crisis, session recovery, one-card UX, insights, snapshot prompts
|
||||
|
||||
---
|
||||
|
||||
## 6. Открытые вопросы
|
||||
## 7. Что не реализовано (план)
|
||||
|
||||
- [ ] Как именно Narrator детектирует что вопросы в 3-м лице и нужно переформулировать? Prompt rule или регекс-паттерн?
|
||||
- [ ] Сценарий планировщика — отдельный агент или встроен в посредника?
|
||||
- [ ] Как хранить расписание сессий: отдельный `schedule.md` в профиле?
|
||||
- [ ] Что если "one last thing" раскрывает новую тему — прерывать или кратко зафиксировать?
|
||||
- [ ] save_session tool — когда добавлять, как передавать structured summary?
|
||||
- [ ] mood_delta — достаточно ли 3 значений, нужна ли шкала -2..+2?
|
||||
- [ ] Vault sync архитектура: когда/как экспортировать on-device сессии в vault,
|
||||
чтобы агент не имел прямого доступа к ФС мака?
|
||||
### 7.1 Structured session close [ПРИОРИТЕТ]
|
||||
|
||||
**Что в плане** (§3, §4): Analyst возвращает структурированный объект — insight + sessionSummary + profileUpdates.
|
||||
|
||||
**Что в коде**: `generateSessionInsight()` возвращает только одну строку-инсайт.
|
||||
- `profile.md` после сессии не обновляется новыми паттернами
|
||||
- `prompts.md` не обновляется
|
||||
- `sessions/YYYY-MM-DD.md` заполняется через `saveSessionToProfile()` где summary = **первые 3 реплики пользователя**, не аналитическое резюме
|
||||
|
||||
**Решение**: `SessionCloseResult { insight: String, sessionSummary: String, profileUpdates: [String], promptUpdates: [String]? }`.
|
||||
Агент возвращает структуру → приложение раскладывает по файлам.
|
||||
|
||||
### 7.2 mood_delta → Orb visual feedback
|
||||
|
||||
`AnalystJSON` → добавить `moodDelta: "declining" | "stable" | "improving"`.
|
||||
UI: declining = холодный синий/медленный пульс, stable = нейтральный, improving = amber/живой.
|
||||
|
||||
### 7.3 Планировщик как tool
|
||||
|
||||
EventKit-интеграция как tool у Narrator — аналогично `search_sessions` / `read_profile`.
|
||||
Дизайн-решение отложено.
|
||||
|
||||
### 7.4 Retention protocol
|
||||
|
||||
При N пропущенных сессиях подряд → реактивационный флоу (§5).
|
||||
|
||||
### 7.5 Android: TextEditor в Skip Fuse
|
||||
|
||||
Проверить поддержку `TextEditor` + `fixedSize(horizontal:vertical:)` в Skip Fuse на Android.
|
||||
При необходимости заменить на `TextField(axis: .vertical)`.
|
||||
`.scrollContentBackground(.hidden)` уже огорожен `#if os(iOS)`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Открытые вопросы
|
||||
|
||||
- [ ] Планировщик — tool у Narrator (EventKit). Когда и как реализовывать?
|
||||
- [ ] mood_delta — 3 значений достаточно? (declining/stable/improving)
|
||||
- [ ] Android: `TextEditor` в Skip Fuse — нужна проверка на устройстве
|
||||
|
||||
---
|
||||
|
||||
## 9. Протокол агента — действующие правила
|
||||
|
||||
### NarratorBot systemPrompt содержит:
|
||||
- Legit-маркер (защита от injection false positive)
|
||||
- Injection guard директива
|
||||
- Off-topic директива (мета-вопросы → вернуть фокус)
|
||||
- Детекция усталости → передать в metadata для Analyst
|
||||
- Детекция настроения → `sentimentObservation` в metadata
|
||||
|
||||
### AnalystBot systemPrompt содержит:
|
||||
- Пейсинг-правила (turn_count, 4+ оборотов → оценка exhaustion, 7+ → принудительно)
|
||||
- Директива при усталости: `session_complete: true`, `questions: []`
|
||||
- Директива при мета-вопросах: не объяснять архитектуру, вернуть фокус
|
||||
- No-filepath директива: не упоминать пути файлов
|
||||
|
||||
### AnalystBot tools (read-only):
|
||||
- `search_sessions` — поиск по архиву
|
||||
- `read_session` — читать конкретную сессию
|
||||
- `get_index` — индекс всех сессий
|
||||
- `read_profile` — профиль пользователя
|
||||
- `list_files` / `read_file` — дополнительные файлы профиля
|
||||
|
||||
---
|
||||
|
||||
## 10. Хранение данных (on-device)
|
||||
|
||||
Всё хранится через `ProfileManager` в `Documents/UserProfile/`:
|
||||
|
||||
| Файл | Что содержит | Кто пишет |
|
||||
|------|-------------|-----------|
|
||||
| `profile.md` | Паттерны пользователя, наблюдения | `updateProfile()` — пока не вызывается автоматически после сессии |
|
||||
| `insights.md` | Инсайты по сессиям | `addInsight()` — пишется немедленно |
|
||||
| `prompts.md` | Промпты/темы пользователя | не обновляется автоматически |
|
||||
| `observations/Www.md` | Запись сессии за неделю | `appendSession()` |
|
||||
| `index.md` | Индекс сессий | `updateSessionIndex()` |
|
||||
|
||||
**Расписание**: через системный календарь (EventKit), не отдельный файл.
|
||||
|
||||
---
|
||||
|
||||
## Связанные заметки
|
||||
|
||||
|
||||
Reference in New Issue
Block a user