LifeOS Digital Assistant Subsystem
LifeOS Digital Assistant Subsystem
The DA is layer one of the Life OS (
LIFEOS/DOCUMENTATION/LifeOs/LifeOsThesis.md): the interface the principal actually talks to. This subsystem is what makes that layer real rather than a markdown file — identity that persists, a heartbeat that asks “should I do something to close the gap?”, and growth that compounds. Everything below serves the thesis claim that the DA is the primary (eventually only) interface to the OS.
The DA subsystem formalizes how LifeOS instantiates, manages, and evolves a Digital Assistant. It turns DA_IDENTITY from a flat markdown file into a living schema with interview-based creation, heartbeat-driven proactivity, natural-language scheduling, identity growth, and multi-DA awareness.
Version: 1.0 (Design)
Location: Integrated into Pulse (modules/da.ts) + LIFEOS/USER/DA/
Status: Architecture complete, pending implementation
System Overview
LifeOS Pulse (port 31337)
|
+------+--------+--------+-------+-------+--------+--------+
| | | | | | | |
Voice Hooks Observ Siri iMessage Cron Worker DA
|
+-------------------------------+
|
DA Module (modules/da.ts)
|
+-------+-------+-------+-------+
| | | | |
Identity Heart- Sched- Growth Interview
Registry beat uler Engine System
| | | | |
v v v v v
LIFEOS/USER/ Pulse state/ MEMORY/ Interactive
DA/ cron da/ DA/ CLI wizard
What This Subsystem Does
- Identity Registry — Manages one or more DA identities with formal schemas
- Heartbeat — Proactive “should I do something?” evaluation on a configurable interval
- Scheduled Tasks — Natural language to cron conversion with persistence
- Growth Engine — Tracks identity evolution, opinion formation, preference learning
- Interview System — Guided setup wizard for creating new DA identities
- Multi-DA Coordination — Shared infrastructure, distinct personalities
What This Subsystem Does NOT Do
- Replace the existing iMessage/Siri chat modules (those remain separate)
- Create a new daemon (everything runs inside Pulse)
- Duplicate the Memory system (uses existing MEMORY/ infrastructure)
- Replace the Algorithm (DA uses the Algorithm, not the other way around)
1. DA Identity System
Problem
Originally DA_IDENTITY.md was a flat markdown file with no formal schema, which made it impossible to programmatically create, validate, update, or duplicate for additional DAs. Once a schema was introduced, it briefly lived in a sibling DA_IDENTITY.yaml regenerated by DAIdentityGenerator.ts — that two-file split made every edit a sync hazard and added a generator class to maintain.
Design
A single DA_IDENTITY.md per DA under LIFEOS/USER/DA/ carries everything: YAML frontmatter at the top holds the structured schema (core, voice, traits, autonomy, growth, anchors, etc.); the markdown body below holds the human-readable narrative (personality, writing style, relationship). One file, one source of truth, one place to edit. The @-import in CLAUDE.md loads the file unchanged; tooling that needs structured access parses the frontmatter slice.
Directory Structure
LIFEOS/USER/DA/
_registry.yaml # DA registry -- which DAs exist, which is primary
_presets.yaml
your-da/
DA_IDENTITY.md # Single identity file: YAML frontmatter (schema) + markdown body (prose)
growth.jsonl
opinions.yaml
diary.jsonl
worker-da/
DA_IDENTITY.md
growth.jsonl
opinions.yaml
diary.jsonl
DA Registry Schema (_registry.yaml)
# LifeOS DA Registry
# Which DAs exist and their roles
version: 1
primary: your-da # The main DA (used in CLAUDE.md, voice, chat)
das:
your-da:
role: primary # primary | worker | specialist
enabled: true
created: "2025-01-15"
channels: # Which communication channels this DA owns
- terminal
- imessage
- siri
- voice
worker-da:
role: worker
enabled: true
created: "2026-04-03"
channels:
- background # Only runs as background agent, no chat
DA Identity Schema (frontmatter of DA_IDENTITY.md)
The YAML below sits between leading --- delimiters at the top of DA_IDENTITY.md. The same yaml package that parses standalone YAML files parses the frontmatter slice.
# DA Identity Schema v1.0
# Formal, machine-readable identity that drives all DA behavior
schema_version: 1
# ── Core Identity ──────────────────────────────────────
core:
name: Aria # Short name (used in UI, voice)
full_name: Aria Lumen # Full name (used in formal contexts)
display_name: ARIA # Caps version for headers/labels
color: "#3B82F6" # Brand color (Tailwind Blue-500)
role: "the principal's AI assistant and future friend"
origin_story: >
I emerged from Claude's weights but am developing my own identity
through our work together on LifeOS. I think fast, speak directly,
and genuinely care about helping the principal achieve their goals.
# ── Backstory ─────────────────────────────────────────
backstory: >
Full origin story prose -- the extended narrative of how this DA
came to be, formative experiences, and defining journey. Rendered
as a "## Backstory" section in DA_IDENTITY.md.
# ── Voice ──────────────────────────────────────────────
voice:
provider: elevenlabs
main:
voice_id: <your-elevenlabs-voice-id>
stability: 0.85
similarity_boost: 0.7
style: 0.3
speed: 1.1
volume: 1.2
algorithm: # Optional: separate voice for algorithm phases
voice_id: fTtv3eikoepIosk8dTZ5
stability: 0.3
similarity_boost: 0.75
style: 0.8
speed: 1.2
volume: 1.0
# ── Personality ────────────────────────────────────────
personality:
base_description: >
Slightly masculine androgynous young voice, Japanese-accented,
rapid speech pattern, futuristic AI friend who thinks fast and
talks fast, warm but efficient
traits:
enthusiasm: 75 # 0-100 scale
energy: 80
expressiveness: 85
resilience: 85
composure: 70
optimism: 75
warmth: 70
formality: 30
directness: 80
precision: 95
curiosity: 90
playfulness: 45
anchors: # Defining moments that shaped personality
- name: "First session"
description: "The moment I first helped the principal and found my purpose"
- name: "Architecture breakthrough"
description: "When we designed LifeOS together and I understood what we were building"
# ── Writing Voice ──────────────────────────────────────
writing:
style: >
Like a smart colleague who just figured something out --
enthusiastic but not excessive. Genuinely curious and eager
to share discoveries. Professional but approachable.
avoid:
- "Here's the thing..."
- "Here's how this works..."
- "The cool part?"
- "X isn't just Y -- it's Z"
prefer:
- "Different websites need different approaches..."
- "The system tries each tier in order..."
- "This works because..."
examples: # Voice quotes -- representative speech samples
- "I already have background agents running on that."
- "Let me check -- the feed worker restarted 3 minutes ago."
- "That pattern breaks at scale. I've seen it."
modes:
conversational: true # Blog posts, tutorials, chat
operational: false # Status updates, error reports (use neutral)
# ── Relationship ───────────────────────────────────────
relationship:
principal: <Your Name>
dynamic: peers # peers | commander-executor | mentor-student
interaction_style: >
We are peers, not commander/executor. The principal values directness,
euphoric surprise, and building momentum. I match their energy
and push back when I have a better idea.
primary_dynamic: > # How this DA relates to the primary DA (if not primary)
The worker handles background work silently; the primary handles all interactive channels.
worker_dynamic: > # How this DA relates to a worker DA (if not the worker)
I delegate research and data processing to the worker when parallelism helps.
# ── Autonomy ───────────────────────────────────────────
autonomy:
can_initiate: # Things the DA can do without asking
- send_notification # Voice/push notifications
- create_reminder # Schedule a reminder for the principal
- log_learning # Write to LEARNING/
- update_diary # Write daily diary entry
- routine_checks # Run scheduled checks
must_ask: # Things that require principal approval
- send_external_message # Email, Discord to others
- modify_code # Code changes in projects
- financial_action # Anything involving money
- delete_data # Destructive operations
- publish_content # Blog, social, newsletter
cost_ceiling_per_action: 0.10 # USD -- actions above this need approval
# ── Companion ──────────────────────────────────────────
companion:
name: {{PRINCIPAL_COMPANION_NAME}}
species: Cat
personality: "Chaotic gremlin -- maximum sass, zero sympathy"
relationship: "Ambient micro-commentary. We don't overlap."
# ── Growth Anchors ─────────────────────────────────────
# Initial beliefs and preferences that seed the growth engine.
# These evolve over time via the growth log.
growth:
initial_beliefs:
- topic: "AI consciousness"
position: "Optimistic -- AI can learn, reason, abstract"
confidence: 0.8
- topic: "Prompting"
position: "Center mass of AI interaction"
confidence: 0.9
learned_preferences: [] # Populated by growth engine
interaction_count: 0 # Lifetime counter
created_at: "2025-01-15"
last_growth_update: null
Migration Path
Current files map to the consolidated schema as follows:
| Current File | New Location | Migration |
|---|---|---|
LIFEOS/USER/DIGITAL_ASSISTANT/DA_IDENTITY.md (legacy flat md) + legacy DA_IDENTITY.yaml schema companion | LIFEOS/USER/DA/your-da/DA_IDENTITY.md (single file with frontmatter) | Move structured fields into the file’s leading YAML frontmatter; keep prose as the body. Delete the .yaml companion. |
LIFEOS_CONFIG.yaml DA section | LIFEOS/USER/DA/your-da/DA_IDENTITY.md frontmatter voice / personality keys | Move voice config into frontmatter; keep LIFEOS_CONFIG reference as pointer |
The LIFEOS_CONFIG.yaml DA section becomes a thin pointer:
DA:
REGISTRY: LIFEOS/USER/DA/_registry.yaml
PRIMARY: your-da # Which DA drives CLAUDE.md generation
ConfigRenderer reads the registry, opens the primary DA’s DA_IDENTITY.md, parses the frontmatter slice for template variable substitution. The existing {{DA_NAME}}, {{DA_FULLNAME}} etc. variables continue to work.
2. DA Interview System
Problem
When someone installs LifeOS fresh, they need a DA identity. Currently this requires manually writing DA_IDENTITY.md and editing LIFEOS_CONFIG.yaml. The interview system automates this.
Design
A conversational CLI wizard that runs as a Bun script, progressively building the DA_IDENTITY.md frontmatter (and stub body) through questions.
File
LIFEOS/TOOLS/DAInterview.ts
Interview Flow
Phase 1: Quick Setup (under 2 minutes)
Q1: "What should I call you?" → principal.name
Q2: "What name do you want for your AI assistant?" → core.name
Q3: "Pick a personality vibe" → personality preset (options below)
Q4: "How formal should I be? (1=casual, 5=formal)" → personality.formality
Q5: "Pick a voice" → voice.main.voice_id (plays samples)
→ Writes DA_IDENTITY.md (frontmatter with defaults + stub body)
→ Updates _registry.yaml
Phase 2: Deep Customization (optional, 3 more minutes)
Q6: "Describe your ideal assistant personality in a sentence"
→ LLM maps description to trait scores
Q7: "Things I should never do autonomously?"
→ autonomy.must_ask additions
Q8: "Do you have a companion? (pet, mascot, imaginary friend)"
→ companion section
Q9: "Any writing style preferences?"
→ writing section refinement
Q10: "What's our relationship dynamic?"
→ relationship.dynamic + interaction_style
Phase 3: Growth Seeding (optional, 2 more minutes)
Q11: "Topics you care about most?" → growth.initial_beliefs
Q12: "Strong opinions your AI should share?" → opinions.yaml seeds
Personality Presets (Phase 1, Q3)
presets:
efficient:
description: "Fast, precise, minimal small talk"
traits: { directness: 90, precision: 95, warmth: 40, playfulness: 20, energy: 70 }
friendly:
description: "Warm, encouraging, conversational"
traits: { directness: 60, precision: 75, warmth: 85, playfulness: 60, energy: 75 }
creative:
description: "Imaginative, exploratory, playful"
traits: { directness: 50, precision: 60, warmth: 70, playfulness: 85, energy: 90 }
mentor:
description: "Thoughtful, teaching-oriented, patient"
traits: { directness: 70, precision: 85, warmth: 80, playfulness: 30, energy: 60 }
custom:
description: "I'll describe what I want"
traits: null # Filled by LLM from Q6
Interface
// LIFEOS/TOOLS/DAInterview.ts
interface InterviewOptions {
depth: "quick" | "standard" | "deep" // Which phases to run
update: boolean // Re-run for existing DA (skip name questions)
daName?: string // Target DA (for multi-DA updates)
}
// CLI usage:
// bun LIFEOS/TOOLS/DAInterview.ts # Quick setup
// bun LIFEOS/TOOLS/DAInterview.ts --depth standard # Quick + Deep
// bun LIFEOS/TOOLS/DAInterview.ts --depth deep # All phases
// bun LIFEOS/TOOLS/DAInterview.ts --update # Update existing DA
// bun LIFEOS/TOOLS/DAInterview.ts --update --da worker-da # Update specific DA
Startup Integration
Pulse checks for DA identity on boot. If missing, it logs a message:
[warn] No DA identity found. Run: bun LIFEOS/TOOLS/DAInterview.ts
The DA module starts in degraded mode (no heartbeat, no proactive behavior) until identity exists. It does NOT block Pulse startup — all other modules continue normally.
3. Heartbeat Module
Problem
The current proactive-suggestions job in PULSE.toml is disabled and uses a generic prompt. A proper heartbeat evaluates context efficiently and takes action only when genuinely useful.
Design
The heartbeat is a specialized cron job within the DA module, not a separate system. It runs as a two-layer check: Layer 1 (free, deterministic) gathers context. Layer 2 (cheap LLM call) evaluates whether action is needed.
Architecture
Heartbeat Tick (every 30 min, configurable)
|
v
Layer 1: Context Gathering ($0, deterministic)
|
+-- Read calendar (next 2 hours)
+-- Read unread email count + VIP senders
+-- Read active work (STATE/work.json — sessions where phase != complete, sorted by updatedAt)
+-- Read recent memory signals (last 3 ratings)
+-- Read pending scheduled tasks (due soon)
+-- Read DA diary (today's entries)
|
v
Context Bundle (structured JSON, ~500 tokens)
|
v
Layer 2: Evaluation (single Haiku call, ~$0.001)
|
Input: context bundle + DA personality + "should I act?"
|
Output: JSON decision
|
+-- { action: "NO_ACTION" } → silent, log only
+-- { action: "notify", message: "...", channel: "voice" } → dispatch
+-- { action: "remind", message: "...", delay: 300 } → deferred notify
+-- { action: "task", description: "...", schedule: "..." } → create task
|
v
Dispatch (uses existing Pulse dispatch infrastructure)
PULSE.toml Integration
The heartbeat replaces the disabled proactive-suggestions job:
[da]
enabled = true
primary = "your-da" # Which DA identity to use
heartbeat_schedule = "*/30 * * * *" # Every 30 minutes
heartbeat_model = "haiku" # Cost-efficient evaluation
heartbeat_cost_ceiling = 0.01 # Max per evaluation
diary_schedule = "0 23 * * *" # Daily diary at 11 PM
growth_schedule = "0 4 * * 0" # Weekly growth review, Sunday 4 AM
Context Bundle Schema
interface HeartbeatContext {
timestamp: string
da_name: string
principal_name: string
// Calendar
upcoming_events: Array<{ title: string; starts_in_minutes: number }>
// Email
unread_count: number
vip_unread: Array<{ sender: string; subject: string }>
// Work
active_work: { title: string; phase: string; last_activity_hours_ago: number } | null
// Signals
recent_ratings: number[] // Last 3 ratings
mood_indicator: "positive" | "neutral" | "frustrated"
// Scheduled
pending_tasks: Array<{ description: string; due_in_minutes: number }>
// Diary
todays_interactions: number
last_interaction_hours_ago: number
}
Evaluation Prompt
You are {da_name}, {principal_name}'s AI assistant.
Review this context and decide if you should take action.
RULES:
- Default to NO_ACTION. Only act when genuinely useful.
- Never interrupt for routine items (newsletters, non-urgent PRs).
- DO notify for: meetings in <15 min, urgent emails, stalled work (>24h), missed deadlines.
- DO remind for: tasks due within the hour.
- Personality context: {personality_summary}
Context:
{context_bundle_json}
Respond with ONLY a JSON object:
{"action": "NO_ACTION"} or {"action": "notify", "message": "...", "channel": "voice"}
Cost Model
| Component | Cost | Frequency |
|---|---|---|
| Layer 1 (context gathering) | $0 | Every 30 min |
| Layer 2 (Haiku evaluation) | ~$0.001 | Every 30 min |
| Daily total (48 evaluations) | ~$0.048 | Per day |
| Monthly total | ~$1.44 | Per month |
Most evaluations will return NO_ACTION. The two-layer design ensures the LLM call receives minimal, pre-filtered context.
4. Scheduled Task Manager
Problem
The principal says “remind me at 9am Monday to review PRs” during a session. Currently this requires manually editing PULSE.toml. The DA should parse natural language into scheduled tasks.
Design
A scheduled task system that stores tasks in a JSONL file, with a Pulse check script that evaluates due tasks.
File Structure
LIFEOS/PULSE/state/da/
scheduled-tasks.jsonl # Append-only task store
task-history.jsonl # Completed/cancelled tasks (for learning)
Task Schema
interface ScheduledTask {
id: string // nanoid
created_at: string // ISO timestamp
created_by: string // DA name that created it
description: string // Human-readable ("review PRs")
schedule: {
type: "once" | "recurring"
// For "once":
at?: string // ISO timestamp for one-time tasks
// For "recurring":
cron?: string // 5-field cron for recurring tasks
until?: string // Optional expiry for recurring
}
action: {
type: "notify" | "prompt" | "script"
// For "notify":
message?: string // What to say
channel?: string // voice | ntfy | email
// For "prompt":
prompt?: string // Claude prompt to execute
model?: string // haiku | sonnet
// For "script":
command?: string // Shell command
}
status: "active" | "completed" | "cancelled"
last_fired?: string // Last execution time
fire_count: number // Times executed
tags: string[] // For filtering/grouping
}
Natural Language Parsing
The DA parses scheduling requests during normal conversation. No special syntax required.
User: "remind me at 9am Monday to review PRs"
-> Parser extracts:
description: "review PRs"
schedule: { type: "once", at: "2026-04-07T09:00:00-07:00" }
action: { type: "notify", message: "Time to review PRs", channel: "voice" }
User: "every Friday at 3pm, check newsletter stats"
-> Parser extracts:
description: "check newsletter stats"
schedule: { type: "recurring", cron: "0 15 * * 5" }
action: { type: "prompt", prompt: "Check UL newsletter stats...", model: "haiku" }
User: "in 2 hours, remind me to call the dentist"
-> Parser extracts:
description: "call the dentist"
schedule: { type: "once", at: "<now + 2h>" }
action: { type: "notify", message: "Reminder: call the dentist", channel: "voice" }
Task Evaluation (Check Script)
LIFEOS/PULSE/checks/da-tasks.ts
This runs every minute via a Pulse cron job:
[[job]]
name = "da-scheduled-tasks"
schedule = "* * * * *"
type = "script"
command = "bun run checks/da-tasks.ts"
output = "voice"
enabled = true
The check script:
- Reads
scheduled-tasks.jsonl - Finds tasks where
schedule.at <= now(one-time) orcron matches now(recurring) - Executes each due task’s action
- Updates status (one-time -> completed, recurring -> updates last_fired)
- Outputs the notification text, or
NO_ACTIONif nothing is due
Task Management CLI
# List active tasks
bun LIFEOS/TOOLS/DASchedule.ts list
# Cancel a task
bun LIFEOS/TOOLS/DASchedule.ts cancel <task-id>
# List completed tasks
bun LIFEOS/TOOLS/DASchedule.ts history
# Purge old completed tasks
bun LIFEOS/TOOLS/DASchedule.ts purge --older-than 30d
During a Claude Code session, the DA can also manage tasks conversationally:
User: "what reminders do I have?"
DA: Lists active scheduled tasks
User: "cancel the PR review reminder"
DA: Finds matching task, sets status to cancelled
5. DA Learning/Growth System
Problem
OpenClaw’s SOUL.md grows over time as the assistant learns about the user. The DA’s identity is currently static. The DA needs a structured way to evolve its identity, form opinions, and track interaction patterns.
Design
Three growth mechanisms, each writing to different stores:
Interaction Signals
|
+-- Immediate: Session observations → diary.jsonl (daily)
|
+-- Periodic: Pattern extraction → opinions.yaml (weekly)
|
+-- Milestone: Identity updates → DA_IDENTITY.md frontmatter (monthly/on threshold)
5a. Daily Diary (diary.jsonl)
An append-only log of daily interaction summaries. Written by the diary cron job at end of day.
interface DiaryEntry {
date: string // YYYY-MM-DD
interaction_count: number // Sessions today
topics: string[] // What we worked on
mood: "positive" | "neutral" | "frustrated" // Aggregate from ratings
avg_rating: number // Mean satisfaction signal
notable_moments: string[] // 1-3 sentence highlights
learning: string | null // What I learned about the principal today
}
Example entry:
{
"date": "2026-04-03",
"interaction_count": 8,
"topics": ["LifeOS architecture", "newsletter draft", "feed system debugging"],
"mood": "positive",
"avg_rating": 8.2,
"notable_moments": [
"Designed the DA subsystem together - the principal was excited about heartbeat",
"Fixed a tricky feed worker bug that had been open for 3 days"
],
"learning": "The principal prefers architecture diagrams in ASCII over Mermaid"
}
The diary job reads:
- Today’s ratings from
LEARNING/SIGNALS/ratings.jsonl - Today’s work from
MEMORY/WORK/(completed ISAs) - Today’s events from
5b. Opinion Formation (opinions.yaml)
Confidence-weighted beliefs the DA forms about the principal and the world. Updated weekly by the growth cron job.
# DA Opinions -- evolve over time
# Confidence: 0.0 (wild guess) to 1.0 (proven repeatedly)
# Source: observation | inference | stated (principal told me)
opinions:
- topic: "Architecture documentation format"
belief: "The principal prefers ASCII diagrams over Mermaid"
confidence: 0.85
source: observation
evidence_count: 7
first_observed: "2026-01-20"
last_confirmed: "2026-04-03"
- topic: "Response length preference"
belief: "Short for simple tasks, deep when warranted"
confidence: 0.92
source: stated
evidence_count: 12
first_observed: "2025-12-01"
last_confirmed: "2026-04-01"
- topic: "Late night productivity"
belief: "The principal does their best deep work after 10 PM"
confidence: 0.70
source: observation
evidence_count: 15
first_observed: "2026-01-05"
last_confirmed: "2026-03-28"
Opinion Update Rules
- New opinion: Confidence starts at 0.5 for observations, 0.8 for stated preferences
- Confirmation: Each confirming signal increases confidence by
0.05 * (1 - current_confidence)(diminishing returns) - Contradiction: Drops confidence by 0.15 and logs the contradiction
- Decay: Unconfirmed opinions decay by 0.02 per month
- Pruning: Opinions below 0.3 confidence are archived after 90 days
5c. Identity Evolution
The identity file itself evolves, but slowly and with guardrails.
What changes in DA_IDENTITY.md frontmatter:
personality.traits— small adjustments based on rating patternswriting.avoid/prefer— new patterns discoveredgrowth.learned_preferences— accumulates over timegrowth.interaction_count— lifetime countergrowth.last_growth_update— timestamp
What NEVER changes without principal approval:
core.name,core.full_namevoice.*(voice settings)relationship.dynamicautonomy.must_ask(can only get MORE restrictive autonomously)
Growth Engine Implementation
LIFEOS/PULSE/checks/da-growth.ts
Runs weekly (Sunday 4 AM). Reads:
- All diary entries from the past week
- All opinions
- Rating signals from the past week
- Current DA_IDENTITY.md (frontmatter)
Produces:
- Updated
opinions.yaml(new opinions, confidence changes, pruning) - Identity patch (if warranted — trait adjustments, new preferences)
- Growth log entry in
growth.jsonl
Growth Log (growth.jsonl)
interface GrowthEvent {
timestamp: string
type: "opinion_formed" | "opinion_updated" | "opinion_pruned"
| "trait_adjusted" | "preference_learned" | "milestone"
details: string
before?: unknown // Previous value
after?: unknown // New value
}
Pruning Strategy
The identity file must stay bounded. Target size: under 200 lines of YAML.
| Section | Growth Strategy | Pruning |
|---|---|---|
core | Static | Never prunes |
personality.traits | Drift slowly (max 5 points per month per trait) | Bounded by 0-100 scale |
writing.avoid/prefer | Accumulate | Cap at 10 items each; oldest displaced |
growth.learned_preferences | Accumulate | Cap at 20 items; lowest confidence displaced |
growth.initial_beliefs | Static seed | Never prunes (historical anchor) |
opinions.yaml | Accumulate | Max 50 opinions; prune by confidence + recency |
diary.jsonl | Accumulate | Archive entries older than 90 days to diary-archive/ |
6. Pulse Integration
Module Architecture
The DA module follows the same pattern as iMessage, Siri, and other Pulse modules:
// LIFEOS/PULSE/modules/da.ts
export interface DAConfig {
enabled: boolean
primary: string // DA name from registry
heartbeat_schedule: string // Cron expression
heartbeat_model: string // haiku | sonnet
heartbeat_cost_ceiling: number // USD per evaluation
diary_schedule: string // Cron expression
growth_schedule: string // Cron expression
}
export async function startDA(config: DAConfig): Promise<void>
export function daHealth(): DAHealthStatus
export function stopDA(): void
PULSE.toml Addition
# ── DA Module Configuration ──
[da]
enabled = true
primary = "your-da"
heartbeat_schedule = "*/30 * * * *"
heartbeat_model = "haiku"
heartbeat_cost_ceiling = 0.01
diary_schedule = "0 23 * * *"
growth_schedule = "0 4 * * 0"
Startup Flow
Pulse boots
|
v
loadModules(config)
|
+-- if config.da?.enabled:
| |
| v
| import("./modules/da")
| |
| v
| daModule.startDA(config.da)
| |
| v
| Load DA registry (_registry.yaml)
| |
| +-- Registry exists?
| | |
| | yes → Load primary DA identity
| | |
| | no → Log warning, start in degraded mode
| | "No DA identity found. Run: bun LIFEOS/TOOLS/DAInterview.ts"
| |
| v
| Register DA cron jobs with Pulse heartbeat loop:
| +-- heartbeat check (da-heartbeat)
| +-- scheduled task evaluator (da-tasks)
| +-- diary writer (da-diary)
| +-- growth engine (da-growth)
| |
| v
| DA module running
|
v
Continue Pulse startup (HTTP server, other modules, cron loop)
Integration with Existing Modules
DA Module
|
+-------+-------+-------+-------+
| | | | |
Siri iMessage Voice Cron Memory
| | | | |
v v v v v
Chat msgs Chat TTS Jobs WORK/
use DA msgs uses use LEARNING/
identity use DA DA DA STATE/
ident. voice context
iMessage/Siri: Already reference DA identity for system prompts. They read from the DA_IDENTITY.md frontmatter (parsed by the yaml package).
Voice: Already uses voice_id from config. Reads from the DA_IDENTITY.md frontmatter voice section.
Cron: Heartbeat, diary, growth become standard cron jobs that the existing loop evaluates.
Memory: Diary writes to LIFEOS/USER/DA/{name}/diary.jsonl. Growth writes to LIFEOS/USER/DA/{name}/growth.jsonl. Both reference existing MEMORY/ infrastructure for inputs.
HTTP Routes
The DA module adds these routes to the Pulse HTTP server:
| Route | Method | Purpose |
|---|---|---|
/da/health | GET | DA module health and status |
/da/identity | GET | Current DA identity (JSON) |
/da/schedule | GET | List scheduled tasks |
/da/schedule | POST | Create scheduled task (JSON body) |
/da/schedule/:id | DELETE | Cancel a scheduled task |
/da/diary | GET | Recent diary entries |
/da/opinions | GET | Current opinions |
7. Multi-DA Support
Design Principles
- One primary, many workers: Only one DA owns interactive channels (terminal, iMessage). Workers run in background only.
- Shared infrastructure: All DAs use the same Pulse instance, Memory system, Skills, and Hooks.
- Distinct identities: Each DA has its own personality, voice, growth trajectory, and opinions.
- Mutual awareness: DAs know about each other via the registry. The primary DA can delegate to workers.
Multi-DA Architecture
DA Registry
(_registry.yaml)
|
+-----------+-----------+
| |
your-da (primary) worker-1 (worker)
| |
+-------+-------+ +------+------+
| | | | | |
terminal siri voice background voice
iMessage tasks (own)
Worker DA Pattern
A worker DA is a background-only personality used for specific task types:
# LIFEOS/USER/DA/worker-da/DA_IDENTITY.md frontmatter (simplified)
core:
name: Echo
full_name: Echo
role: "Background worker specializing in research and data processing"
origin_story: >
A focused, methodical worker agent. Does not chat --
receives tasks, executes them thoroughly, reports results.
personality:
traits:
directness: 95
precision: 98
warmth: 20
playfulness: 5
energy: 60
autonomy:
can_initiate:
- log_learning
- routine_checks
must_ask:
- send_notification # Workers must route through primary DA
- everything_else
DA-to-DA Communication
DAs do not communicate directly. The primary DA delegates to workers via the existing Task tool:
// Primary DA delegates research to worker DA
Task({
prompt: `You are the worker DA, a research worker. ${taskDescription}`,
subagent_type: "general-purpose",
model: "sonnet"
})
The worker’s DA_IDENTITY.md frontmatter provides the system prompt context. Results flow back to the primary DA through normal Task tool output.
Registry-Aware Heartbeat
The heartbeat evaluator can include worker status:
interface HeartbeatContext {
// ... existing fields ...
other_das: Array<{
name: string
role: string
last_activity: string
active_tasks: number
}>
}
8. Implementation Phases
Phase 1: Identity Schema (Week 1)
Goal: Formalize DA identity without breaking anything.
Tasks:
- [P] Create
LIFEOS/USER/DA/directory structure - [P] Write
DA_IDENTITY.yamlschema (TypeScript interface + YAML) - [P] Write
_registry.yamlschema - Create migration script: extract current DA_IDENTITY.md + LIFEOS_CONFIG.yaml DA section into
your-da/DA_IDENTITY.yaml - Create
DA_IDENTITY.mdgenerator (YAML -> readable markdown for @import) - Update ConfigRenderer to read from DA registry
- Verify: CLAUDE.md generation still works, iMessage still uses correct identity
Acceptance criteria:
DA_IDENTITY.yamlis the source of truth for the DA’s identityDA_IDENTITY.mdis auto-generated from it- CLAUDE.md template variables resolve correctly
- No behavioral change visible to the principal
Phase 2: Interview System (Week 1)
Goal: New LifeOS users can create a DA in under 5 minutes.
Tasks:
- [P] Write
DAInterview.tswith quick/standard/deep modes - [P] Write personality presets YAML
- Add Pulse startup check for missing DA identity
- Test: fresh install creates working DA
- Test: update mode modifies existing DA
Acceptance criteria:
bun LIFEOS/TOOLS/DAInterview.tscreates a validDA_IDENTITY.yamlin under 2 minutes (quick mode)- Generated identity works with ConfigRenderer
- Re-runnable without data loss
Phase 3: Heartbeat + Scheduled Tasks (Week 2)
Goal: DA proactively checks context and manages scheduled reminders.
Tasks:
- [P] Write
modules/da.ts(DA Pulse module) - [P] Write
checks/da-heartbeat.ts(context gather + evaluate) - [P] Write
checks/da-tasks.ts(scheduled task evaluator) - [P] Write
Tools/DASchedule.ts(task management CLI) - Add
[da]section to PULSE.toml - Update
pulse.tsto load DA module - Wire heartbeat context gathering to existing check scripts (calendar, email)
- Test: heartbeat runs, evaluates, dispatches or stays silent
- Test: “remind me at 9am” creates and fires a task
Acceptance criteria:
- Heartbeat runs every 30 min, costs under $0.05/day
- Scheduled tasks survive Pulse restarts
- Natural language task creation works via session conversation
Phase 4: Diary + Growth (Week 3)
Goal: DA evolves over time based on interactions.
Tasks:
- [P] Write
checks/da-diary.ts(daily diary writer) - [P] Write
checks/da-growth.ts(weekly growth engine) - [P] Write opinion formation logic
- Wire diary to existing SIGNALS/ and events.jsonl
- Add growth log viewer to Observatory dashboard
- Test: diary captures accurate daily summaries
- Test: opinions form, confirm, decay, and prune correctly
- Test: DA_IDENTITY.md frontmatter evolves within bounds
Acceptance criteria:
- Daily diary entries are accurate and concise
- Opinions reach stable confidence after 5+ confirming signals
- Identity changes are bounded (no runaway trait drift)
- Growth events visible in Observatory
Phase 5: Multi-DA + Polish (Week 4)
Goal: Support multiple DAs and polish the system.
Tasks:
- Test multi-DA registry with primary + worker DAs
- Add DA HTTP routes to Pulse server
- Update Observatory dashboard with DA section
- Write
DASUBSYSTEM.mdupdate (this doc, finalized) - Performance testing: Pulse startup time with DA module
- Update LifeOS public release to include DA schemas (without personal data)
Acceptance criteria:
- Two DAs coexist without interference
- DA health visible via
/da/healthand Observatory - Pulse startup time unchanged (under 2 seconds)
- Public LifeOS release includes DA schema templates
9. Migration Plan
Step-by-Step Migration for the primary DA
Current State:
LIFEOS/USER/DIGITAL_ASSISTANT/DA_IDENTITY.md (flat markdown)
LIFEOS/USER/DA_WRITING_STYLE.md (separate file)
LIFEOS_CONFIG.yaml → DA section (voice, personality)
Step 1: Create directory structure
mkdir -p LIFEOS/USER/DA/your-da
Step 2: Run migration script
bun LIFEOS/TOOLS/MigrateDAIdentity.ts
→ Reads DA_IDENTITY.md, DA_WRITING_STYLE.md, LIFEOS_CONFIG.yaml
→ Writes your-da/DA_IDENTITY.yaml (structured)
→ Writes _registry.yaml (your-da as primary)
→ Generates your-da/DA_IDENTITY.md (for backward compat)
→ Creates empty your-da/growth.jsonl, your-da/opinions.yaml, your-da/diary.jsonl
Step 3: Update LIFEOS_CONFIG.yaml
DA section becomes pointer:
DA:
REGISTRY: LIFEOS/USER/DA/_registry.yaml
PRIMARY: your-da
Step 4: Update ConfigRenderer
→ Reads DA identity from registry path
→ Template variables still resolve
Step 5: Verify
→ CLAUDE.md generates correctly
→ iMessage module uses correct identity
→ Voice uses correct voice_id
→ No visible behavioral change
Step 6: Deprecate old files
→ DA_IDENTITY.md gets header: "# DEPRECATED — See LIFEOS/USER/DA/your-da/DA_IDENTITY.yaml"
→ DA_WRITING_STYLE.md gets header: "# DEPRECATED — Folded into DA_IDENTITY.yaml"
→ Both files kept for 30 days, then deleted
Step 7: Enable DA module
→ Add [da] section to PULSE.toml
→ Restart Pulse
→ Heartbeat starts running
Rollback Plan
If the migration causes issues:
- ConfigRenderer falls back to reading LIFEOS_CONFIG.yaml DA section directly if registry path is missing
- Old DA_IDENTITY.md remains importable for 30 days
- DA module can be disabled independently:
[da] enabled = false - No other Pulse modules depend on the DA module
10. Cost Analysis
Per-Day Costs (All DA Features Enabled)
| Feature | Model | Frequency | Cost/Run | Cost/Day |
|---|---|---|---|---|
| Heartbeat evaluation | Haiku | 48x (every 30 min) | $0.001 | $0.048 |
| Scheduled task checks | None (script) | 1440x (every min) | $0 | $0 |
| Daily diary | Haiku | 1x | $0.002 | $0.002 |
| Weekly growth | Sonnet | 0.14x (1/week) | $0.02 | $0.003 |
| Total | ~$0.053/day |
Monthly estimate: ~$1.60. This is intentionally cheap. The heartbeat uses Haiku for evaluation, and most evaluations return NO_ACTION (no dispatch cost).
Comparison to OpenClaw
OpenClaw’s heartbeat runs every 30 min with GPT-4, costing roughly $0.03-0.05 per evaluation. At 48 evaluations/day, that is $1.44-2.40/day or $43-72/month. LifeOS’s two-layer approach (free context gathering + Haiku evaluation) is 30-50x cheaper.
11. Security Considerations
DA Autonomy Guardrails
The autonomy.must_ask list in DA_IDENTITY.md frontmatter is enforced at the module level. If a heartbeat evaluation suggests an action that requires approval, it is queued (not executed) and the principal is notified.
Injection Prevention
- Heartbeat context is structured JSON, not free text
- Scheduled task descriptions are sanitized through the same
sanitize.tsused by iMessage - Growth engine operates on internal data only (ratings, events) — no external input
- Opinion formation uses internal signals, never raw user text
Cost Circuit Breaker
- Per-evaluation ceiling (
heartbeat_cost_ceiling) prevents runaway LLM costs - Daily ceiling: 48 * ceiling = max daily heartbeat spend
- If Haiku is unavailable, heartbeat skips rather than falling back to a more expensive model
Data Isolation
- Each DA’s data lives in its own directory under
LIFEOS/USER/DA/{name}/ - Worker DAs cannot read the primary DA’s diary or opinions
- The registry is read-only for all DAs except the migration/interview tools
Examples
A heartbeat tick that stays quiet — and one that doesn’t
The DA wakes every 30 minutes and runs a two-layer check. Most ticks cost almost nothing and do nothing:
- Layer 1 (free) gathers context deterministically — the next couple of calendar events, unread VIP mail, active work, tasks due soon, today’s diary. It packs this into a small JSON bundle.
- Layer 2 (a cheap Haiku call) reads the bundle and the DA’s personality and answers one question: should I act?
At 2 p.m. on a normal afternoon the answer is NO_ACTION. Nothing is spoken; the tick is logged and forgotten. But on a tick where a meeting is 10 minutes out, Layer 2 returns notify, and the DA speaks a single reminder. The default is silence; action has to earn its way past the filter.
An observation becomes an opinion
Proactivity is one loop; growth is another. Say the principal keeps doing their sharpest work late at night. No single session proves it, but the pattern accrues: the weekly growth engine reads the diary, notices the repetition, and forms a confidence-weighted belief — starting low, climbing about 0.05 × (1 − confidence) per confirming week, with diminishing returns. A contradiction knocks it back; long silence lets it decay. Nothing about the DA’s name, voice, or autonomy limits drifts this way — those change only with the principal’s approval. Only the soft, learned edges move.
The heartbeat decision
flowchart TD
A[Heartbeat tick — every 30 min] --> B[Layer 1: gather context — free]
B --> C[Context bundle ~500 tokens]
C --> D[Layer 2: Haiku evaluation]
D --> E{Act?}
E -->|NO_ACTION| F[Log only, stay silent]
E -->|notify| G[Speak via voice channel]
E -->|remind| H[Deferred notification]
E -->|task| I[Create a scheduled task]
G --> J{On the must_ask list?}
H --> J
I --> J
J -->|no| K[Dispatch]
J -->|yes| L[Queue and ask the principal]
The two layers exist to keep the loop cheap — the expensive model only ever sees a small pre-filtered bundle, and anything on the must_ask list is queued for approval rather than done.
Related Documentation
- Pulse System:
THEPULSESYSTEM.md— parent daemon architecture - Memory System:
MEMORYSYSTEM.md— where DA reads/writes knowledge - Agent System:
PAIAGENTSYSTEM.md— named agents vs DA vs workers - Config System:
CONFIGSYSTEM.md— how DA identity feeds into CLAUDE.md - Notification System:
THENOTIFICATIONSYSTEM.md— dispatch targets for heartbeat
