* feat(agent): fuzzy-filter skills_agent toolkit actions by task prompt
Narrow large Composio toolkits (e.g. github ~500 actions) down to the
handful relevant to a given delegation prompt before registering them
as native tools on a spawned skills_agent. Falls back to the full
catalogue when the filter yields fewer than MIN_CONFIDENT_HITS hits to
avoid starving the sub-agent on under-specified prompts.
Filter is only invoked when both `definition.id == "skills_agent"` and
a `toolkit=` argument is present, so orchestrator and other sub-agents
are unaffected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(composio): mock toolkits route in ops integration test
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 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