Data Flow
How data moves through the Parachute system: from user input to AI response, from voice to transcript, from app to vault.
Chat Message Flow
When a user sends a chat message, here's how it flows through the system:
┌──────────────────────────────────────────────────────────────────────────┐
│ 1. USER INPUT │
│ │
│ User types message in ChatScreen │
│ ↓ │
│ ChatInput widget captures text + attachments │
│ ↓ │
│ chatMessagesNotifier.sendMessage() │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 2. APP STATE UPDATE │
│ │
│ Add optimistic user message to state │
│ Add empty assistant message (for streaming) │
│ ↓ │
│ ChatService.sendMessage() → HTTP POST /api/chat │
│ ↓ │
│ Open SSE connection for streaming response │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 3. SERVER RECEIVES REQUEST │
│ │
│ POST /api/chat │
│ ├── Validate auth (API key or localhost) │
│ ├── Parse ChatRequest │
│ └── Call orchestrator.run_streaming() │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 4. ORCHESTRATOR PREPARES │
│ │
│ Load agent definition (or default vault-agent) │
│ Get or create session: │
│ ├── New: Create pending placeholder, SDK will assign ID │
│ └── Resume: Load from DB, verify SDK session exists │
│ Build system prompt: │
│ ├── Agent system prompt │
│ ├── Context files/folders │
│ └── Working directory context │
│ Process attachments: │
│ ├── Save to vault/Chat/assets/{date}/ │
│ └── Build attachment message parts │
│ Check permissions: │
│ ├── Session grants (Read, Write, Bash, MCP) │
│ └── Global deny patterns │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 5. CLAUDE SDK CALL │
│ │
│ claude_sdk.query_streaming(): │
│ ├── Override HOME to vault path (for .claude/ isolation) │
│ ├── Build SDK query options (model, max_tokens, permissions) │
│ └── Call SDK with async generator │
│ │
│ SDK connects to Claude AI: │
│ ├── Sends conversation history │
│ ├── Processes tool calls internally │
│ └── Streams response chunks │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 6. STREAMING RESPONSE │
│ │
│ SDK yields events → Orchestrator yields SSE events │
│ │
│ Event types: │
│ ├── session: {id, title, created_at} — First event │
│ ├── text: {content} — Text chunk │
│ ├── thinking: {content} — Thinking content │
│ ├── tool_use_start: {id, name, input} — Tool beginning │
│ ├── tool_result: {tool_use_id, result} — Tool completed │
│ ├── message_stop: {} — Message complete │
│ └── error: {message} — Error occurred │
│ │
│ Each event sent as: │
│ data: {"type": "text", "content": "Hello"} │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 7. APP RECEIVES & UPDATES │
│ │
│ ChatService parses SSE events │
│ ├── session: Save session ID to state │
│ ├── text: Append to assistant message content │
│ ├── thinking: Add to thinking section │
│ ├── tool_use_start: Add ToolCall to message │
│ ├── tool_result: Update ToolCall with result │
│ └── message_stop: Mark message complete │
│ │
│ UI updates reactively via Riverpod: │
│ MessageBubble renders streaming content │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 8. SESSION PERSISTENCE │
│ │
│ After message complete: │
│ │
│ Server: │
│ ├── session_manager.update() — Update DB metadata │
│ ├── SDK writes to ~/.claude/projects/.../*.jsonl │
│ └── Activity hook triggers (title + summary generation) │
│ │
│ App: │
│ ├── Update local session state │
│ └── Session available for resume │
└──────────────────────────────────────────────────────────────────────────┘
Vault Structure
The vault is your local-first data store. All Parachute data lives in plain files at ~/Parachute:
~/Parachute/
├── Daily/ # Journal entries (local-first)
│ ├── 2026-01-28.md # Today's journal
│ ├── 2026-01-27.md # Yesterday's journal
│ ├── assets/ # Media files
│ │ ├── 2026-01-28/
│ │ │ ├── audio-10-30-am.m4a
│ │ │ └── photo-10-45-am.jpg
│ │ └── ...
│ ├── reflection/ # Reflection agent outputs
│ │ └── 2026-01-28.md
│ ├── content-scout/ # Content-scout outputs
│ │ └── 2026-01-28.md
│ ├── .agents/ # Custom daily agents
│ │ └── my-agent.md
│ └── .agents/ # Daily agent configs
│ └── content-scout.md
│
├── Chat/ # Chat sessions (server-managed)
│ ├── sessions.db # SQLite metadata
│ ├── assets/ # Chat attachments
│ │ └── 2026-01-28/
│ │ └── uploaded-file.pdf
│ └── contexts/ # Saved context configurations
│ └── my-project.json
│
├── .claude/ # Claude SDK data
│ ├── projects/ # Session transcripts (JSONL)
│ │ └── {project-hash}/
│ │ └── {session-id}.jsonl
│ └── settings.json # SDK settings
│
├── .parachute/ # Parachute config
│ ├── server-config.json # Auth settings, API keys
│ ├── agents/ # Global agent definitions
│ │ └── vault-agent.md
│ └── skills/ # Skill plugins
│ └── my-skill.md
│
└── ... (user files) # Rest of your vault
Pointer Architecture: The SQLite database (
sessions.db) stores only metadata. Actual message transcripts live in SDK JSONL files. This makes the database a pointer layer, not the source of truth for message content.
Para-ID System
Portable identifiers for cross-device sync. Every journal entry gets a unique ID that follows it everywhere.
Journal Entry Format
# para:abc123def456 10:30 AM
Walked to the coffee shop this morning. Had a great idea about
the project architecture...
## Images

## Audio
- Duration: 2:30
- Path: ../assets/2026-01-28/audio-10-30-am.m4a
Format Specification
- Pattern:
para:{12-char-alphanumeric} - Example:
para:abc123def456 - Location: H1 header of entry section
- Generated: On entry creation, never changes
- Scope: Unique across all modules (Daily, Chat, etc.)
Sync Flow
Device A Server Device B
│ │ │
│ Create entry │ │
│ para:abc123def456 │ │
│ │ │
│ ────── Sync (push) ──────────▶│ │
│ Hash: sha256:123... │ │
│ │ │
│ │◀────── Sync (pull) ────────────│
│ │ Request manifest │
│ │ │
│ │─────── Manifest ───────────────▶│
│ │ para:abc123def456 │
│ │ Hash: sha256:123... │
│ │ │
│ │◀────── Fetch entry ────────────│
│ │ │
│ │─────── Entry content ──────────▶│
│ │ # para:abc123def456 10:30 AM │
│ │ ... │
│ │ │
│ │ Entry saved
│ │ Same para:ID
Voice Transcription Flow
Local-first transcription using Sherpa-ONNX with streaming display:
┌──────────────────────────────────────────────────────────────────────────┐
│ 1. AUDIO CAPTURE │
│ │
│ Microphone → record package → PCM audio stream │
│ ↓ │
│ Audio chunks (16kHz, 16-bit, mono) │
│ ↓ │
│ StreamingVoiceService receives chunks │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 2. VOICE ACTIVITY DETECTION │
│ │
│ SimpleVAD analyzes audio levels │
│ ├── Below threshold: Silence │
│ └── Above threshold: Speech detected │
│ ↓ │
│ SmartChunker groups speech into segments │
│ ├── Min segment: 1 second │
│ ├── Max segment: 30 seconds │
│ └── Split on silence > 500ms │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 3. TRANSCRIPTION (Sherpa-ONNX) │
│ │
│ Audio segment → SherpaOnnxService │
│ ├── Runs in isolate (background thread) │
│ ├── Uses Parakeet model (465MB) │
│ └── Returns: TranscriptionResult │
│ │
│ LocalAgreement-2 algorithm: │
│ ├── Re-transcribe every 2 seconds │
│ ├── Compare consecutive transcriptions │
│ ├── Confirm text stable across 2+ iterations │
│ └── Display: [confirmed] + [tentative] + [interim] │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 4. STATE UPDATE │
│ │
│ StreamingTranscriptionState: │
│ ├── confirmed: "I went to the store" ← Stable, won't change │
│ ├── tentative: "and bought some" ← Likely stable │
│ └── interim: "coff" ← Still processing │
│ │
│ UI shows: "I went to the store and bought some coff▌" │
└────────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────────┐
│ 5. SAVE TO JOURNAL │
│ │
│ User stops recording → Final transcription │
│ ↓ │
│ JournalService.saveEntry(): │
│ ├── Generate para:ID │
│ ├── Save audio to Daily/assets/{date}/ │
│ ├── Write markdown entry with transcript │
│ └── Update journal day file │
└──────────────────────────────────────────────────────────────────────────┘
Session Lifecycle
New Session
- User sends first message (no session_id)
- Orchestrator creates "pending" placeholder
- SDK executes and generates session ID
- SessionManager creates DB record with SDK session ID
- SDK writes transcript to
~/.claude/projects/.../session.jsonl - Activity hook triggers title generation
Resume Session
- User opens existing session
- App loads session from provider (cached or fetched)
- User sends message with session_id
- SessionManager looks up DB record
- SDK resumes with existing session ID
- Conversation continues from where it left off
Imported Session (Claude/ChatGPT)
- User uploads JSON export file
- ImportService parses format (auto-detects source)
- Converts messages to SDK JSONL format
- Creates session in DB with source flag
- Optionally enables "continuation" mode
- User can continue conversation with full context
Background Processing
Automated agents and hooks process content in the background:
┌────────────────────────────────────────────────────────────────────────────┐
│ ACTIVITY HOOK (Post-Session) │
│ │
│ Triggered: After each chat exchange completes │
│ Purpose: Generate/update title, summarize to daily chat log │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Session │ ───▶ │ Summarizer │ ───▶ │ update_title() │ │
│ │ completes │ │ Agent │ │ write_chat_log │ │
│ │ │ │ (one-shot) │ │ track_metrics │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │
│ Output: Daily/chat-log/{date}.md │
└────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ DAILY AGENTS (Scheduled via APScheduler) │
│ │
│ Location: Daily/.agents/*.md │
│ Schedule: Time from YAML frontmatter (default 3:00 UTC) │
│ Runner: core/daily_agent.py + core/scheduler.py │
│ │
│ Example agent (content-scout.md): │
│ --- │
│ schedule: "8:00" │
│ schedule_enabled: true │
│ tools: [read_journal, web_search] │
│ --- │
│ Find relevant news and articles based on journal interests... │
│ │
│ Scheduler discovers agents → Runs at scheduled times → Writes outputs │
│ │
│ State: Daily/.{agent-name}/state.json (tracks last run date) │
└────────────────────────────────────────────────────────────────────────────┘
Streaming Protocol
SSE (Server-Sent Events) for real-time responses:
SSE Event Stream Example
# Connection opened to POST /api/chat
data: {"type": "session", "id": "sess_abc123", "title": "New Chat"}
data: {"type": "prompt_metadata", "model": "claude-sonnet-4", "context_files": 2}
data: {"type": "text", "content": "I'll help you "}
data: {"type": "text", "content": "with that. Let me "}
data: {"type": "tool_use_start", "id": "tool_1", "name": "Read", "input": {"file_path": "/src/main.py"}}
data: {"type": "tool_result", "tool_use_id": "tool_1", "result": "# Main entry point..."}
data: {"type": "text", "content": "I can see the main file. "}
data: {"type": "thinking", "content": "The user wants to understand the architecture..."}
data: {"type": "text", "content": "Here's what I found:"}
data: {"type": "message_stop"}
# Connection closed
Event Types
| Type | When | Content |
|---|---|---|
session |
First event | Session ID, title, created_at |
prompt_metadata |
After session | Model, context files count |
text |
During response | Text content chunk |
thinking |
During response | Thinking/reasoning content |
tool_use_start |
Tool beginning | Tool ID, name, input |
tool_result |
Tool completed | Tool ID, result/error |
message_stop |
Response complete | Empty (signals end) |
error |
On error | Error message |