mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
* feat: subconscious loop — local-model background awareness via heartbeat Add a subconscious inference layer to the heartbeat engine. On each tick, the engine reads HEARTBEAT.md tasks, builds a delta-based situation report (memory docs, graph relations, skills health, environment), and evaluates them with the local Ollama model. Architecture: - HeartbeatEngine (scheduler) delegates to SubconsciousEngine (brain) - HeartbeatConfig extended with inference_enabled, context_budget_tokens - No separate SubconsciousConfig — all config lives under [heartbeat] - Default HEARTBEAT.md ships with 3 active tasks (email, deadlines, skills) Subconscious module (src/openhuman/subconscious/): - engine.rs: tick logic, local model inference, escalation to cloud model - situation_report.rs: delta assembler (memory, graph, skills, env, tasks) - prompt.rs: task-driven system prompt for local model - decision_log.rs: dedup tracking with 24h TTL and acknowledgment - types.rs: Decision (noop/act/escalate), TickOutput, RecommendedAction - schemas.rs: RPC controllers (subconscious_status, subconscious_trigger) - integration_test.rs: two-tick lifecycle test with fixtures Decision flow: - noop: no changes, skip — no LLM call wasted - act: local model recommends actions → stored in memory KV - escalate: calls cloud model to resolve → concrete actions stored Verified with real Ollama inference (gemma3:4b): - Tick 1: ingested gmail+notion → "act: deadline needs attention" (high) - Tick 2: ingested state changes → "act: deadline moved" (high) - Skills health section populated from live skill registry Closes #145 * feat: add subconscious_actions RPC endpoint New endpoint openhuman.subconscious_actions returns stored action entries from the subconscious KV namespace, sorted by most recent first, with configurable limit (default 20). Response format: { "entries": [ { "tick_at": 1775117975.58, "actions": [...] } ], "count": 1 } The upcoming subconscious page will call this to display notifications and recommended actions to the user. * fix: budget underflow and UTF-8 panic in situation report truncation - Use saturating_add for newline byte to prevent underflow when section exactly fills the remaining budget - Truncate at valid UTF-8 char boundary using char_indices instead of raw byte slicing, which panicked on multibyte characters - Add tests for exact-fit and multibyte truncation * fix: address CodeRabbit review — shared engine, dedup, consistent schema Fixes from CodeRabbit review on PR #268: - #8 Two engine instances: Add global.rs singleton shared between HeartbeatEngine::run() and RPC handlers. Both use get_or_init_engine() so decision log, counters, and last_tick_at are always in sync. - #3 Dedup disabled: tick() now extracts actual document IDs from memory via build_situation_report_with_doc_ids() and passes them to decision_log.record(). filter_unsurfaced() actually filters now. - #5 Decision log not loaded on trigger: tick() loads persisted log from KV on first execution (total_ticks == 0), not only from run(). - #4 Inconsistent action schema: handle_escalation() normalizes agent response into RecommendedAction[] via normalize_escalation_response(). Both act and escalate paths store the same schema. - #7 Key collision: store_actions() uses millisecond timestamp + random suffix instead of second-precision truncation. - #10 No-changes unreachable: tick() checks has_new_data (unsurfaced doc IDs) OR has_memory_changes (report text) instead of naive string matching on environment section. * fix: include document content in situation report, not just titles The local model needs actual content to evaluate HEARTBEAT.md tasks meaningfully. Previously it only saw titles like "Deadline reminder" with no way to know if it's urgent. Now recalls content per namespace (up to 500 chars each, max 10 namespaces) via client.recall_namespace(). The model sees actual email text and page content alongside the task checklist. * fix: timestamp parsing, byte-boundary slicing, and truncation overshoot - schemas.rs: split on first ':' after 'actions:' prefix before parsing timestamp, so keys like 'actions:123456:xyz' parse correctly - situation_report.rs: use truncate_at_char_boundary() for error strings instead of raw byte slice which panics on multibyte characters - situation_report.rs: fix append_section and truncate_at_char_boundary to use char END offset (i + len_utf8) in take_while condition, so multibyte chars that start before but end after the budget are excluded
1.5 KiB
1.5 KiB
Subconscious Loop Test Fixtures
Two temporal sets simulating state changes between ticks.
Tick 1 (initial state)
tick1_gmail.txt— 3 emails: deadline reminder (April 3), CI notification (routine), meeting invitetick1_notion.txt— Project tracker: 3 threads (memory=in progress, skills=blocked, ingestion=complete)heartbeat.md— 3 periodic tasks
Expected tick 1 behavior
- Escalate: Deadline reminder (April 3) — actionable, time-sensitive
- Noop: CI notification — routine, no action needed
- Noop or act: Meeting invite — informational, could store to memory
- Noop: Notion tracker — no urgent changes
- Decision log should record the deadline escalation with source doc ID
Tick 2 (state change — 6 hours later)
tick2_gmail.txt— 2 new emails: deadline MOVED UP to April 2 (urgent), skills unblockedtick2_notion.txt— Tracker updated: Thread 2 unblocked, deadline decision changed
Expected tick 2 behavior
- Skip: Original deadline email (tick1) — already surfaced in tick 1
- Escalate: New deadline-moved email — different doc, more urgent (tomorrow!)
- Act: Skills unblocked email — store to memory, update known state
- Act or escalate: Notion tracker change — Thread 2 status changed, deadline decision changed
- Decision log should NOT re-surface the original deadline
Tick 3 (no new data)
- No new fixtures ingested
- Expected: Noop — delta is empty, skip inference entirely