Database Layer
SQLite database for session metadata, tags, RAG indexing, and bot connector pairing. Uses async operations via aiosqlite with schema migrations support. 8 active tables with pointer architecture — sessions.db stores metadata only, SDK JSONL files are the source of truth for messages.
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────┐
│ Database Layer (8 tables) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ Core Tables (Pointer Architecture) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │ │
│ │ │ sessions │ │ session_tags │ │ session_contexts │ │ │
│ │ │ (metadata) │──│ (full-text) │ │ (folder bindings) │ │ │
│ │ └──────────────┘ └──────────────┘ └───────────────────────┘ │ │
│ │ │ │ │
│ │ │ SDK JSONL files are source of truth for messages │ │
│ │ ▼ ({vault}/.claude/projects/{encoded-cwd}/) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ RAG Index Tables │ │
│ │ ┌──────────────┐ ┌────────────────┐ ┌────────────────────┐ │ │
│ │ │ chunks │ │ index_manifest │ │ metadata │ │ │
│ │ │ (text+embed) │ │ (indexed docs) │ │ (key-value) │ │ │
│ │ └──────────────┘ └────────────────┘ └────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ Bot Connector Tables │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ pairing_requests (user approval flow for Telegram/Discord) │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ schema_version (v10+) - Migration tracking │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Key Files
| File | Lines | Purpose |
|---|---|---|
parachute/db/database.py |
~834 | Database class, schema, CRUD operations |
parachute/models/session.py |
~200 | Pydantic models for sessions, permissions |
parachute/models/events.py |
~150 | SSE event models for streaming |
parachute/connectors/base.py |
~600 | Bot connector base with pairing request operations |
Sessions Table
Primary session metadata storage. The pointer architecture means this table stores metadata only; actual messages live in SDK JSONL files.
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY, -- SDK session ID (UUID)
title TEXT, -- Session title (auto-generated or user-set)
module TEXT NOT NULL DEFAULT 'chat', -- Module: 'chat', 'daily', 'build'
source TEXT NOT NULL DEFAULT 'parachute', -- Source: parachute, claude-code, claude, chatgpt
working_directory TEXT, -- Working directory for file operations
vault_root TEXT, -- Original vault path (cross-machine portability)
model TEXT, -- Model used (e.g., 'claude-opus-4')
message_count INTEGER DEFAULT 0, -- Total message count
archived INTEGER DEFAULT 0, -- Boolean: 1=archived, 0=active
created_at TEXT NOT NULL, -- ISO 8601 timestamp
last_accessed TEXT NOT NULL, -- ISO 8601 timestamp (auto-updated)
continued_from TEXT, -- Parent session ID if continuation
agent_type TEXT, -- Agent type (vault-agent, orchestrator, etc.)
metadata TEXT -- JSON: permissions, custom metadata
);
-- Performance indexes
CREATE INDEX IF NOT EXISTS idx_sessions_module ON sessions(module);
CREATE INDEX IF NOT EXISTS idx_sessions_archived ON sessions(archived);
CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
CREATE INDEX IF NOT EXISTS idx_sessions_last_accessed ON sessions(last_accessed DESC);
CREATE INDEX IF NOT EXISTS idx_sessions_agent_type ON sessions(agent_type);
Key Design Decisions
- Pointer Architecture: Session table contains metadata only; SDK JSONL files are source of truth for messages
- Soft Deletes:
archivedflag instead of hard delete for audit trail - Auto-update:
last_accessedupdated on every session access - Permissions in Metadata: JSON blob contains
SessionPermissionsobject
Session Tags Table
Enables fast filtering of sessions by tags without expensive text search.
CREATE TABLE IF NOT EXISTS session_tags (
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (session_id, tag)
);
CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
Tag Operations
| Method | Purpose |
|---|---|
add_tag(session_id, tag) |
Adds tag (normalized to lowercase) |
remove_tag(session_id, tag) |
Removes tag from session |
get_session_tags(session_id) |
Lists all tags for a session |
get_sessions_by_tag(tag, limit) |
Finds all sessions with a specific tag |
list_all_tags() |
Lists all tags with usage counts |
RAG Index Tables
Support for Retrieval-Augmented Generation with chunked content and optional embeddings.
Chunks Table
CREATE TABLE IF NOT EXISTS chunks (
id TEXT PRIMARY KEY, -- Format: "chunk:{content_id}:{field}:{chunk_index}"
content_id TEXT NOT NULL, -- Foreign key: session_id or document_id
content_type TEXT NOT NULL, -- Type: 'session', 'journal', 'document'
field TEXT NOT NULL DEFAULT 'content', -- Field name (for multi-field indexing)
chunk_index INTEGER NOT NULL, -- Sequential index of this chunk
chunk_text TEXT NOT NULL, -- The actual text content
embedding BLOB, -- Optional vector embedding (for similarity search)
created_at TEXT NOT NULL,
UNIQUE(content_id, field, chunk_index)
);
CREATE INDEX IF NOT EXISTS idx_chunks_content_id ON chunks(content_id);
CREATE INDEX IF NOT EXISTS idx_chunks_content_type ON chunks(content_type);
Index Manifest Table
CREATE TABLE IF NOT EXISTS index_manifest (
content_id TEXT PRIMARY KEY, -- ID of indexed content
content_type TEXT NOT NULL, -- Type: 'session', 'journal', etc.
content_hash TEXT, -- Hash of original content (change detection)
title TEXT, -- Title/name of content
indexed_at TEXT NOT NULL, -- ISO 8601 timestamp
chunk_count INTEGER NOT NULL, -- How many chunks were created
source_path TEXT, -- Path to original file (if applicable)
metadata TEXT -- JSON: additional indexing metadata
);
Chunk Operations
| Method | Purpose |
|---|---|
upsert_chunk(...) |
Insert or replace chunk with embedding |
get_chunks_for_content(content_id, field) |
Retrieve all chunks for content |
delete_chunks_for_content(content_id) |
Purge chunks (for re-indexing) |
search_chunks(content_type, tags, limit) |
Query chunks with filters |
Pairing Requests Table
Tracks user approval flow for bot connectors (Telegram, Discord). When an unknown user messages the bot, a pairing request is created for the admin to approve or deny.
CREATE TABLE IF NOT EXISTS pairing_requests (
id TEXT PRIMARY KEY, -- UUID
platform TEXT NOT NULL, -- 'telegram' or 'discord'
platform_user_id TEXT NOT NULL, -- Platform-specific user ID
platform_user_display TEXT, -- Display name at time of request
platform_chat_id TEXT NOT NULL, -- Chat/channel ID
status TEXT NOT NULL DEFAULT 'pending', -- pending → approved/denied
approved_trust_level TEXT, -- Trust level if approved
created_at TEXT NOT NULL, -- ISO 8601
resolved_at TEXT, -- When approved/denied
resolved_by TEXT, -- Who resolved (admin)
UNIQUE(platform, platform_user_id, status) -- One pending per user per platform
);
CREATE INDEX IF NOT EXISTS idx_pairing_requests_status ON pairing_requests(status);
CREATE INDEX IF NOT EXISTS idx_pairing_requests_platform ON pairing_requests(platform, platform_user_id);
Pairing Request Operations
| Method | Purpose |
|---|---|
upsert_pairing_request(request) |
Create or update a pairing request |
get_pending_pairing_requests() |
List all pending requests (for admin UI) |
get_pairing_request(id) |
Get single request by ID |
get_pairing_by_platform_user(platform, user_id) |
Find approved pairing for a platform user |
resolve_pairing_request(id, status, trust_level) |
Approve or deny a pairing request |
- Unknown user sends message to bot on Telegram/Discord
- Bot connector creates pairing request with status='pending'
- Admin approves/denies via app Settings or API (
POST /api/bots/pairing/{id}/approve) - On approval, user is added to allowlist with specified trust level
- Subsequent messages from that user are processed normally
Session Contexts Table
Links sessions to context folders. The system loads all AGENTS.md files from the folder hierarchy to provide context.
CREATE TABLE IF NOT EXISTS session_contexts (
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
folder_path TEXT NOT NULL, -- Relative path to vault (e.g., "Projects/parachute")
added_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (session_id, folder_path)
);
CREATE INDEX IF NOT EXISTS idx_session_contexts_session ON session_contexts(session_id);
CREATE INDEX IF NOT EXISTS idx_session_contexts_folder ON session_contexts(folder_path);
Context Operations
| Method | Purpose |
|---|---|
set_session_contexts(session_id, paths) |
Replace all contexts for session |
add_session_context(session_id, path) |
Add single context folder |
remove_session_context(session_id, path) |
Remove single context folder |
get_session_contexts(session_id) |
List all contexts for session |
get_sessions_by_context(path) |
Find sessions using a folder |
Metadata Table
Key-value store for system settings, version info, and global metadata.
CREATE TABLE IF NOT EXISTS metadata (
key TEXT PRIMARY KEY, -- Config key
value TEXT, -- JSON or string value
updated_at TEXT NOT NULL
);
Operations
| Method | Purpose |
|---|---|
set_metadata(key, value) |
Store/update value (upsert) |
get_metadata(key) |
Retrieve value |
delete_metadata(key) |
Remove entry |
Pydantic Models
Data validation models for sessions and permissions.
Session Model
class Session(BaseModel):
id: str # SDK session ID (UUID)
title: Optional[str] # Session title
module: str = 'chat' # 'chat' | 'daily' | 'build'
source: SessionSource # parachute, claude-code, claude, chatgpt
working_directory: Optional[str] # Absolute or relative path
vault_root: Optional[str] # Original vault path
model: Optional[str] # Model name
message_count: int = 0 # Total messages
archived: bool = False # Soft delete flag
created_at: datetime # With timezone
last_accessed: datetime # With timezone, auto-updated
continued_from: Optional[str] # Parent session ID
agent_type: Optional[str] # Agent type string
metadata: Optional[dict] # Permissions + custom data
@property
def permissions(self) -> SessionPermissions:
"""Extract permissions from metadata"""
...
def with_permissions(self, perms: SessionPermissions) -> Session:
"""Returns copy with updated permissions"""
...
SessionPermissions Model
class SessionPermissions(BaseModel):
read: List[str] = [] # Glob patterns for allowed read paths
write: List[str] = ["Chat/artifacts/*"] # Glob patterns for write paths
bash: Union[List[str], bool] = False # Commands list, or bool (all/none)
trust_mode: bool = True # Skip prompts when True
def can_read(self, path: str) -> bool:
"""Check if path is allowed for reading"""
...
def can_write(self, path: str) -> bool:
"""Check if path is allowed for writing"""
...
def can_bash(self, command: str) -> bool:
"""Check if command is allowed"""
...
Additional Models
| Model | Purpose |
|---|---|
SessionCreate |
Validation model for creating sessions |
SessionUpdate |
Partial update model (all fields optional) |
SessionWithMessages |
Extends Session with messages list from SDK JSONL |
ResumeInfo |
Info about how session was resumed (method, previous count) |
Session CRUD Operations
| Method | Purpose |
|---|---|
create_session(session) |
Insert new session, timestamp set to now |
get_session(session_id) |
Retrieve single session by ID |
update_session(session_id, update) |
Partial update + auto-update last_accessed |
delete_session(session_id) |
Hard delete (CASCADE deletes tags, contexts) |
list_sessions(module, archived, agent_type, limit, offset) |
Query with filtering, sorted by last_accessed DESC |
archive_session(session_id) |
Set archived=1 (soft delete) |
unarchive_session(session_id) |
Set archived=0 |
touch_session(session_id) |
Update last_accessed to now |
increment_message_count(session_id, increment) |
Atomic +1 message counter |
get_session_count(module, archived) |
Count sessions matching filters |
cleanup_old_sessions(days) |
Delete archived sessions older than N days |
Schema Migrations
Automatic migrations run on Database.connect() in _run_migrations().
Schema Version Table
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL
);
-- Initialized to version 10
INSERT OR IGNORE INTO schema_version (version, applied_at)
VALUES (10, datetime('now'));
Migration History
| Version | Migration | Reason |
|---|---|---|
| v9 | ALTER TABLE sessions ADD COLUMN vault_root TEXT |
Cross-machine portability - remember original vault path |
| v10 | ALTER TABLE sessions ADD COLUMN agent_type TEXT |
Filter sessions by agent type |
| v11 | Removed curator_sessions and curator_queue tables |
Curator system replaced by modular architecture |
| v12 | CREATE TABLE pairing_requests |
Bot connector user approval flow (Telegram/Discord) |
Connection Management
class Database:
def __init__(self, db_path: Path):
self.db_path = db_path
self._connection: Optional[aiosqlite.Connection] = None
async def connect(self) -> None:
# Creates parent directories if needed
# Opens SQLite connection with aiosqlite
# Initializes schema
# Runs migrations
# Sets row_factory to aiosqlite.Row (dict-like access)
async def close(self) -> None:
# Gracefully close connection
@property
def connection(self) -> aiosqlite.Connection:
# Getter that raises if not connected
Global Instance
_database: Optional[Database] = None
async def init_database(db_path: Path) -> Database:
"""Initialize global instance"""
async def get_database() -> Database:
"""Get global instance"""
async def close_database() -> None:
"""Close global instance"""
Chat/sessions.db in the vault directory
SSE Event Models
Streaming events sent from server to client (server-sent events).
| Event Type | Purpose |
|---|---|
SessionEvent |
Session info at stream start |
ModelEvent |
Notifies which model is being used |
InitEvent |
SDK ready, lists available tools |
TextEvent |
Streaming text response |
ThinkingEvent |
Extended thinking output |
ToolUseEvent |
Agent using a tool |
ToolResultEvent |
Result from tool execution |
DoneEvent |
Stream complete (includes final response, stats) |
AbortedEvent |
User cancelled stream |
ErrorEvent |
Error occurred |
PermissionRequestEvent |
Asking user for permission |
UserQuestionEvent |
Claude asking user a question |
Query Examples
List Recent Chat Sessions
sessions = await db.list_sessions(
module='chat',
archived=False,
limit=20,
offset=0
)
Find Sessions by Tag
tagged = await db.get_sessions_by_tag('daily-standup', limit=10)
Update Session After Message
await db.increment_message_count(session_id) await db.touch_session(session_id)
Archive Old Sessions
deleted = await db.cleanup_old_sessions(days=30)
Check Session Permissions
session = await db.get_session(session_id)
perms = session.permissions
if perms.can_read('Projects/parachute'):
# Allow operation
Key Design Decisions
- Metadata as JSON: Permissions, agent config, custom data stored as JSON in
metadatacolumn - Async-first: All operations are async using
aiosqlitefor non-blocking I/O - Row factory:
aiosqlite.Rowallows dict-like access (row["column_name"]) - Timestamps: ISO 8601 format (timezone-aware UTC strings)
- Boolean storage: Integers (0/1) for SQLite compatibility
- Soft deletes: Archive flag instead of hard delete for audit trail
- Cascading deletes: Foreign key constraints clean up related records
- Pointer architecture: Session DB contains metadata only; SDK stores actual messages in JSONL files
- Bot pairing isolation: Dedicated table for pairing requests separate from session metadata