mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 21:44:38 +00:00
* feat(subconscious): stabilize heartbeat + subconscious loop (#392) - Enable heartbeat by default (enabled=true, inference_enabled=true, 5min interval) - Seed system tasks on engine init, not first tick - SQLite-backed task/log/escalation persistence - Overlap guard with generation counter — stale ticks are cancelled - Single log entry per task per tick, updated in place (in_progress → act/noop/escalate/failed/cancelled) - Rate-limit retry (429 only) for agentic-v1 cloud model calls - Approval gate: unsolicited write actions on read-only tasks require user approval - Analysis-only mode for agentic-v1 on read-only escalations - Non-blocking status RPC — reads from DB, never blocks on engine mutex - Frontend: system vs user task distinction, toggle switches, expandable activity log - Frontend: 3s auto-poll on Subconscious tab, skill-related escalation navigation - Consecutive failure counter in status (resets on success) - last_tick_at only advances on successful evaluation - Missing LLM evaluation fallback — unevaluated tasks default to noop - Docs: subconscious.md architecture guide, memory-sync-functions.md reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier formatting for subconscious frontend files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: retrigger checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(heartbeat): use disabled config in run_returns_immediately_when_disabled test HeartbeatConfig::default() has enabled: true, so run() entered the infinite loop and never returned — hanging the test (and CI) indefinitely. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(subconscious): remove HEARTBEAT.md task import, use SQLite as sole task source Tasks are now managed exclusively in SQLite via the Subconscious UI. HEARTBEAT.md is retained for instructions/context only, not as a task list. Situation report now reads pending tasks from SQLite instead of HEARTBEAT.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: cargo fmt on subconscious engine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
310 lines
11 KiB
Markdown
310 lines
11 KiB
Markdown
# Subconscious Loop
|
|
|
|
Background task evaluation and execution system. Periodically checks user-defined and system tasks against the current workspace state, decides what to do, and either acts autonomously or escalates to the user.
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Heartbeat Engine │
|
|
│ (sleeps N minutes between ticks) │
|
|
└──────────────────────┬──────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Subconscious Engine │
|
|
│ │
|
|
│ 1. Load due tasks from SQLite │
|
|
│ 2. Insert in_progress log entries │
|
|
│ 3. Build situation report (memory + workspace state) │
|
|
│ 4. Evaluate tasks with local Ollama model │
|
|
│ 5. Execute decisions (act / noop / escalate) │
|
|
│ 6. Update log entries in place │
|
|
└─────────────────────────────────────────────────────────┘
|
|
│
|
|
┌───────────┼───────────┐
|
|
▼ ▼ ▼
|
|
noop act escalate
|
|
(skip) (execute) (agentic-v1)
|
|
```
|
|
|
|
### Key files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `src/openhuman/heartbeat/engine.rs` | Periodic scheduler, delegates to subconscious engine |
|
|
| `src/openhuman/subconscious/engine.rs` | Core tick logic, state management, overlap guard |
|
|
| `src/openhuman/subconscious/executor.rs` | Task execution routing (local model vs agentic-v1) |
|
|
| `src/openhuman/subconscious/prompt.rs` | Prompt builders for evaluation and execution |
|
|
| `src/openhuman/subconscious/store.rs` | SQLite persistence (tasks, log, escalations) |
|
|
| `src/openhuman/subconscious/types.rs` | Data types and enums |
|
|
| `src/openhuman/subconscious/situation_report.rs` | Builds context from memory and workspace state |
|
|
| `src/openhuman/subconscious/global.rs` | Global singleton shared between heartbeat and RPC |
|
|
| `src/openhuman/subconscious/schemas.rs` | RPC endpoint handlers |
|
|
| `app/src/hooks/useSubconscious.ts` | Frontend hook for data fetching and actions |
|
|
| `app/src/pages/Intelligence.tsx` | UI rendering (Subconscious tab) |
|
|
| `app/src/utils/tauriCommands/subconscious.ts` | TypeScript RPC wrappers |
|
|
|
|
---
|
|
|
|
## Task Types
|
|
|
|
### System tasks
|
|
|
|
Seeded automatically on engine initialization. Cannot be deleted, only disabled.
|
|
|
|
Default system tasks:
|
|
- Check connected skills for errors or disconnections
|
|
- Review new memory updates for actionable items
|
|
- Monitor system health (Ollama, memory, connections)
|
|
|
|
Additional system tasks are imported from `HEARTBEAT.md` in the workspace directory (one task per `- ` line).
|
|
|
|
### User tasks
|
|
|
|
Added manually via the UI. Can be toggled on/off and deleted.
|
|
|
|
Examples:
|
|
- "Check urgent emails" (read-only)
|
|
- "Send daily summary to Slack" (write intent)
|
|
- "Summarize Notion updates" (read-only)
|
|
|
|
---
|
|
|
|
## Tick Lifecycle
|
|
|
|
### 1. Overlap guard
|
|
|
|
Each tick gets a monotonically increasing generation counter. If a new tick starts while the old one is still running (e.g., slow LLM call), the old tick's results are discarded and its `in_progress` log entries are marked as `cancelled`.
|
|
|
|
The heartbeat uses `tokio::time::sleep` (not `interval`) so ticks never stack up.
|
|
|
|
### 2. Load due tasks
|
|
|
|
Query: enabled, not completed, `next_run_at <= now` or never run.
|
|
|
|
### 3. Log as in_progress
|
|
|
|
Each due task gets a single log row inserted with `decision = "in_progress"`. This row is updated in place as the task progresses — no duplicate rows.
|
|
|
|
### 4. Evaluate with local model
|
|
|
|
The local Ollama model receives all due tasks + a situation report and returns a per-task decision:
|
|
|
|
| Decision | Meaning |
|
|
|----------|---------|
|
|
| `noop` | Nothing relevant right now |
|
|
| `act` | Something relevant found — execute the task |
|
|
| `escalate` | Needs deeper reasoning — hand off to agentic-v1 |
|
|
|
|
### 5. Execute
|
|
|
|
Routing depends on the decision and the task's intent:
|
|
|
|
```
|
|
Decision: noop
|
|
→ Update log to "noop", advance schedule
|
|
|
|
Decision: act
|
|
├─ Task has write intent (needs_tools = true)
|
|
│ → Execute with local model
|
|
│
|
|
└─ Task is read-only
|
|
→ Execute with local model
|
|
|
|
Decision: escalate
|
|
├─ Task has write intent (needs_tools = true)
|
|
│ → Run agentic-v1 with full permissions
|
|
│ → No approval needed (user explicitly asked for the write action)
|
|
│
|
|
└─ Task is read-only
|
|
→ Run agentic-v1 in analysis-only mode
|
|
→ If response contains "RECOMMENDED ACTION:" (unsolicited write)
|
|
│ → Create escalation for user approval
|
|
│ → On approval → run agentic-v1 with full permissions
|
|
└─ Otherwise → log result, done
|
|
```
|
|
|
|
### 6. Update log entry
|
|
|
|
The same row inserted in step 3 is updated to the final state:
|
|
|
|
| Decision | Dot color | Text |
|
|
|----------|-----------|------|
|
|
| `in_progress` | Blue (pulsing) | "Evaluating..." |
|
|
| `act` | Green | Result text |
|
|
| `noop` | Gray | "Nothing new" |
|
|
| `escalate` | Amber | "Waiting for approval" |
|
|
| `failed` | Coral | Error message |
|
|
| `cancelled` | Gray | "Cancelled" |
|
|
| `dismissed` | Gray | "Skipped" |
|
|
|
|
---
|
|
|
|
## Execution Models
|
|
|
|
### Local Ollama model
|
|
|
|
Used for:
|
|
- Task evaluation (all tasks, every tick)
|
|
- Text-only task execution (summarize, check, monitor, review)
|
|
|
|
No cost, no rate limits, runs on-device.
|
|
|
|
### agentic-v1 (cloud)
|
|
|
|
Used for:
|
|
- Tool-required task execution (send, post, delete, create)
|
|
- Analysis-only mode for read-only tasks escalated by the local model
|
|
|
|
Rate-limit retry: up to 3 attempts with exponential backoff (2s, 4s, 8s) on 429 errors.
|
|
|
|
---
|
|
|
|
## Approval Gate
|
|
|
|
Approval is only required when the AI wants to take a **write action that the user didn't explicitly request**.
|
|
|
|
| Task intent | AI wants to write | Approval needed? |
|
|
|-------------|-------------------|-----------------|
|
|
| "Send digest to Slack" (write) | Yes | No — user asked for it |
|
|
| "Check urgent emails" (read) | No | No — read-only result |
|
|
| "Check urgent emails" (read) | Yes (wants to forward them) | **Yes** — unsolicited write |
|
|
|
|
The approval flow:
|
|
1. agentic-v1 runs in analysis-only mode
|
|
2. Response contains `RECOMMENDED ACTION: Forward 3 urgent emails to #team-alerts`
|
|
3. Escalation card appears in UI under "Approval Needed"
|
|
4. User clicks "Go ahead" → agentic-v1 runs again with full permissions
|
|
5. Or user clicks "Skip" → nothing happens
|
|
|
|
### Skill-related escalations
|
|
|
|
Escalations related to skills (detected by keywords: skill, oauth, notion, gmail, integration, disconnect, re-auth) show a "Fix in Skills" button that navigates to the Skills page instead of "Go ahead".
|
|
|
|
---
|
|
|
|
## Failure Handling
|
|
|
|
### Consecutive failure counter
|
|
|
|
Tracked in `EngineState.consecutive_failures`. Increments when the entire LLM evaluation fails (Ollama down, network error). Resets to 0 on any successful tick. Surfaced in the UI status bar as "N failed" in coral.
|
|
|
|
Individual task execution failures do NOT increment this counter — they are logged per-task but the tick itself is considered successful.
|
|
|
|
### last_tick_at advancement
|
|
|
|
`last_tick_at` only advances on successful ticks. If the LLM evaluation fails or the tick is cancelled, `last_tick_at` stays unchanged so the next tick's situation report covers the same time range — nothing is missed.
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
In `config.toml` under `[heartbeat]`:
|
|
|
|
```toml
|
|
[heartbeat]
|
|
enabled = true # Enable the heartbeat loop
|
|
interval_minutes = 5 # Tick interval (minimum 5)
|
|
inference_enabled = true # Enable local model evaluation
|
|
context_budget_tokens = 40000 # Max tokens for situation report
|
|
```
|
|
|
|
Defaults: `enabled = true`, `interval_minutes = 5`, `inference_enabled = true`.
|
|
|
|
---
|
|
|
|
## SQLite Schema
|
|
|
|
Database: `{workspace_dir}/subconscious/subconscious.db`
|
|
|
|
### subconscious_tasks
|
|
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| id | TEXT PK | UUID |
|
|
| title | TEXT | Task description |
|
|
| source | TEXT | `"system"` or `"user"` |
|
|
| recurrence | TEXT | `"pending"`, `"once"`, or `"cron:expr"` |
|
|
| enabled | INTEGER | 1 = active, 0 = paused |
|
|
| last_run_at | REAL | Unix timestamp of last evaluation |
|
|
| next_run_at | REAL | Unix timestamp of next scheduled run |
|
|
| completed | INTEGER | 1 = done (one-off tasks) |
|
|
| created_at | REAL | Unix timestamp |
|
|
|
|
### subconscious_log
|
|
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| id | TEXT PK | UUID |
|
|
| task_id | TEXT | FK to tasks |
|
|
| tick_at | REAL | Unix timestamp of the tick |
|
|
| decision | TEXT | `in_progress`, `act`, `noop`, `escalate`, `failed`, `cancelled`, `dismissed` |
|
|
| result | TEXT | Result text or error message |
|
|
| duration_ms | INTEGER | Execution duration |
|
|
| created_at | REAL | Unix timestamp |
|
|
|
|
### subconscious_escalations
|
|
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| id | TEXT PK | UUID |
|
|
| task_id | TEXT | FK to tasks |
|
|
| log_id | TEXT | FK to log entry |
|
|
| title | TEXT | Escalation title |
|
|
| description | TEXT | What needs approval |
|
|
| priority | TEXT | `critical`, `important`, `normal` |
|
|
| status | TEXT | `pending`, `approved`, `dismissed` |
|
|
| created_at | REAL | Unix timestamp |
|
|
| resolved_at | REAL | When approved/dismissed |
|
|
|
|
---
|
|
|
|
## RPC Endpoints
|
|
|
|
All under `openhuman.subconscious_*`:
|
|
|
|
| Method | Description |
|
|
|--------|-------------|
|
|
| `subconscious_status` | Get engine status (enabled, ticks, failures) |
|
|
| `subconscious_trigger` | Manually trigger a tick (runs in background, returns immediately) |
|
|
| `subconscious_tasks_list` | List all tasks |
|
|
| `subconscious_tasks_add` | Add a user task |
|
|
| `subconscious_tasks_update` | Update task (title, enabled, recurrence) |
|
|
| `subconscious_tasks_remove` | Remove a user task (system tasks can only be disabled) |
|
|
| `subconscious_log_list` | List activity log entries |
|
|
| `subconscious_escalations_list` | List escalations (filterable by status) |
|
|
| `subconscious_escalations_approve` | Approve and execute an escalation |
|
|
| `subconscious_escalations_dismiss` | Dismiss an escalation |
|
|
|
|
---
|
|
|
|
## UI (Intelligence Page → Subconscious Tab)
|
|
|
|
### Status bar
|
|
|
|
Shows: task count, total ticks, last tick time, consecutive failures (if > 0).
|
|
|
|
### Active Tasks
|
|
|
|
- **System tasks**: displayed as plain text with green dot and "default" badge. No controls.
|
|
- **User tasks**: toggle switch (enable/disable) + delete button on hover.
|
|
- **Add task**: text input + "Add" button at the bottom.
|
|
|
|
### Approval Needed
|
|
|
|
Amber cards for pending escalations. Each shows title, description, priority badge.
|
|
- **"Go ahead"**: approve and execute the write action.
|
|
- **"Fix in Skills"**: shown for skill-related escalations, navigates to Skills page.
|
|
- **"Skip"**: dismiss without executing.
|
|
|
|
### Activity Log
|
|
|
|
Chronological list of task evaluations. Each entry shows timestamp, colored dot, and result text. Auto-polls every 2s while any entries are `in_progress`.
|
|
|
|
### Run Now
|
|
|
|
Triggers a manual tick. The tick runs in the background — the RPC returns immediately and the UI polls for updates.
|