mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
f9c15bd1eec240f44bcf1232002f4aab652a0088
570
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e3c46d1c3d |
fix(whatsapp): upgrade WhatsApp Web channel to upstream whatsapp-rust 0.5 (#916)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
238a9a5ad9 |
feat(slack): backfill ingestion + LLM summariser + tree fanout gate (#934)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4f3deb1038 | [UX] Telegram: Calm reasoning placeholders and suppressed thought streaming (#949) | ||
|
|
2e4f1925f6 | Telegram: silent streaming updates to reduce notification spam (#940) | ||
|
|
7867496d86 | feat(home): banners, welcome typewriter, conversation gating (#936) | ||
|
|
e0e7e1bd77 |
refactor: extract inline tests to sibling _tests.rs files (130 god-files) (#835)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
80fbe370c4 | feat(welcome): rework welcome agent — 5-step checklist, in-app modals, faster model (#919) | ||
|
|
579970c559 | feat(conversations): remove suggested-question prompt bubbles (#918) | ||
|
|
7b123bc2fe | feat(onboarding): rework desktop onboarding and welcome timing (#915) | ||
|
|
13242cc9d6 | feat(integrations): stock-price tools and expanded Parallel surface (#911) | ||
|
|
98b74b5a3d | feat(tools): add curl + gitbooks agent tools (#907) | ||
|
|
080deae117 | feat(welcome): split onboarding tool + detect gmail/webview logins (#894) | ||
|
|
215a776eea | feat(ux): lock UI to welcome agent until chat onboarding completes (#883) (#892) | ||
|
|
d2f045769f | test(notifications): add Phase 4 RPC and dismiss reducer coverage (#878) | ||
|
|
1d864efbeb | feat(notifications): unify notification slices, dismiss action, stats panel, unified Notifications page (#876) | ||
|
|
95504d4b09 |
test: add 196 tests across MCP, services, utils, autocomplete (#855)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
9d14006f45 |
fix(cron): spawn scheduler loop at startup so scheduled jobs auto-fire (#873)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
7bd8f9d4c1 | ci: parallelize rust tests, dedupe typecheck/clippy/sentry across workflows (#895) | ||
|
|
a96d9da3a4 | chore: migrate from yarn to pnpm (#886) | ||
|
|
11c67a70b9 | feat(notifications): add dismiss/stats RPC, DomainEvents, and triage bus integration (#875) | ||
|
|
208c276a3e | feat(notifications): bridge CEF providers into core ingest pipeline with dedup (#874) | ||
|
|
b329e45cdb |
feat(skills): uninstall for user-scope SKILL.md skills (#781) (#833)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b81d04df54 | feat(redirect_links): SQLite-backed URL shortener for token-heavy links (#870) | ||
|
|
500ddc2a8a | feat(webview_apis): WebSocket bridge for live-webview APIs, Gmail first (#869) | ||
|
|
b40af6c294 |
fix(security): command injection in npm postinstall + weak RNG fallback (#837)
- Replace `execSync` with `execFileSync` in npm install script to prevent command injection. - Pass PowerShell paths via environment variables to avoid shell metacharacter interpolation. - Add `-NoProfile` and `-NonInteractive` flags to PowerShell extraction for cleaner installs. - Upgrade `makeAccountId` to prioritize `crypto.getRandomValues` over `Math.random` for suffixes. - Update internal thread title logic to import `collapse_whitespace` from the correct location. Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
53befcd56f |
chore: drop dead crypto + stale rules, Windows doctor disk probe (#856)
Two cleanups surfaced during a codebase review: ## Changes - **Drop dead integration-token crypto** — `integrationTokensCrypto.ts` + helpers; no remaining consumers - **Trim stale `.claude/rules`** — rules that contradict current CLAUDE.md - **Windows doctor disk probe** — detect low-disk conditions on Windows - **Refresh stale binary-size claim** in docs Net: +82 / −3,596 (mostly dead-code removal). ## Test plan - [ ] `yarn typecheck` green — no dangling imports to crypto module - [ ] `cargo check` green - [ ] Doctor runs on Windows host Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
1408666706 |
feat(notifications): native OS notifications via CEF shim + scanner fallback (#850)
Co-authored-by: oxoxDev <nikhil@tinyhumans.ai> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
c0f6e39b3e |
feat(memory_tree): Phase 4 retrieval tools for hierarchical memory (#710) (#831)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
9d7237bb91 |
feat(skills): agentic loop wiring for SKILL.md bodies (#781) (#807)
- Add Skill::read_body to fetch SKILL.md instruction text during agent inference. - Implement a skill matcher supporting explicit @ mentions and automatic keyword/tag heuristics. - Create a rendering system for skill injection with an 8KB budget and truncation handling. - Wire skill matching and instruction body injection into the Agent::turn execution path. - Include 24 unit tests for skill matching, ranking logic, and size-cap enforcement. Closes #781 Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
c59b4289f1 |
refactor(config): extract env lookup seam for testability (#792)
## Summary - Introduce an `EnvLookup` seam in `src/openhuman/config/schema/load.rs`. - Route env-overlay reads through `EnvLookup`/`ProcessEnv` while preserving existing precedence and runtime behavior. - Add focused parallel-safe unit coverage for config env overlay behavior without mutating process env. ## Problem - `Config::apply_env_overrides` mixed environment access, override logic, and global side effects. - That made precedence and parsing behavior harder to unit test in isolation and forced reliance on process-env mutation. ## Solution - Add `pub(crate) trait EnvLookup` with `get`, `contains`, and `get_any`, plus a production `ProcessEnv` implementation. - Delegate overlay logic through `apply_env_overlay_with(&ProcessEnv)` and keep side effects in the small wrapper. - Add focused tests covering precedence, parsing, defaults, and legacy-migration interactions with no global env mutation. ## Submission Checklist - [x] **Unit tests** — Targeted Rust config tests added and broader related module suites passed - [ ] **E2E / integration** — Not applicable; this refactor preserves existing behavior and improves unit-level testability - [x] **N/A** — E2E is not applicable because no user-visible or cross-process flow changed - [ ] **Doc comments** — Not applicable; internal refactor in existing module only - [ ] **Inline comments** — Not applicable; logic remains local and test coverage documents behavior ## Impact - No intended runtime behavior change; refactor keeps config precedence intact. - Improves maintainability and makes future config/workspace resolution seams easier to test safely. ## Related - Issue(s): - Follow-up PR(s)/TODOs: thread `EnvLookup` through runtime config directory resolution helpers Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
629b4ccb07 |
test(memory_tree): unit tests for retrieval RPC handlers (#710) (#839)
- Add JSON-RPC handlers for source, global, and topic-based memory retrieval. - Integrate Ollama embeddings for semantic reranking of chunks and summaries. - Implement 15 unit tests for RPC handlers covering parameter parsing and PII redaction. - Redact PII from logs by removing raw source, entity, and node identifiers. - Fix BFS traversal for drill-down and deduplicate results in topic queries. - Add configuration for embedding endpoints, models, and strictness modes. Co-authored-by: sanil-23 <sanil@vezures.xyz> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
c78f496686 | fix(webhooks,cron): wire trigger pipeline so agents auto-respond reliably (#747) | ||
|
|
7670ae9f12 |
feat(notifications): core→shell DomainEvent bridge (Phase 1d of #395) (#782)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
5e660f2fe8 |
feat(core): memory namespaces, recall citations, provider_surfaces RPC (#803)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
29a3962b55 | Feat: add email login auth (#832) | ||
|
|
22b7a8b18f |
[codex] Harden threads schema parse contract tests (#797)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com> |
||
|
|
e86406ea05 |
[codex] Refactor config runtime dir resolution for testability (#794)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com> |
||
|
|
f539899a69 |
refactor(agent): move debug dump into domain and extract dump writer (#764)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
4993a410f2 |
test(rpc,core,billing,team): harden registry + RPC helpers with edge-case coverage (#786)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jwalin Shah <jshah1331@gmail.com> |
||
|
|
1d62d1c0ac |
refactor(threads, tool_timeout): extract pure helpers for testability (#784)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jwalin Shah <jshah1331@gmail.com> |
||
|
|
fc213572c6 |
test(threads): harden JSON-RPC validation coverage (#790)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jwalin Shah <jshah1331@gmail.com> |
||
|
|
325f019853 |
refactor(core): move CLI adapters out of src/core/ into per-domain cli modules (#758)
* refactor(voice): move standalone CLI adapter into voice domain
Move the blocking `openhuman voice` / `openhuman dictate` dictation-server
subcommand out of `src/core/cli.rs` and into a new `src/openhuman/voice/cli.rs`
owned by the voice domain. The core CLI dispatcher now routes to
`voice::cli::run_standalone_subcommand` and no longer imports voice internals.
Why this shape: the standalone server blocks forever on the hotkey listener,
which doesn't fit the controller registry's request/response contract. A
domain-owned CLI adapter is the right home for long-lived operational
commands; the controller registry stays for RPC-style capabilities.
Establishes the exposure rule documented in PLAN.md for the rest of the
backend overhaul.
- src/openhuman/voice/cli.rs: new 118-line adapter (flag parsing, tokio
runtime, run_standalone call)
- src/openhuman/voice/mod.rs: declare `pub mod cli`
- src/core/cli.rs: delete 92-line run_voice_server_command, dispatch only
- PLAN.md: new scope doc + exposure rule
* refactor(text_input): move CLI adapter into domain
Move src/core/text_input_cli.rs to src/openhuman/text_input/cli.rs and
drop the stale `pub mod text_input_cli` from src/core/mod.rs. Core CLI
dispatcher now routes `text-input` to the domain-owned adapter.
The moved file has a mixed shape that validates the exposure rule from
the previous voice commit:
- `run` starts a long-lived HTTP JSON-RPC server (domain cli.rs shape)
- `read / insert / ghost / dismiss` are short-lived UX wrappers around
`text_input::rpc::*` (pretty-print, flag parsing — CLI affordance, not
transport duplication)
Both shapes legitimately belong in the domain. The controller registry
stays for transport-agnostic capabilities; CLI UX wrappers live next to
their domain RPC.
- git mv src/core/text_input_cli.rs src/openhuman/text_input/cli.rs
- src/openhuman/text_input/mod.rs: declare `pub mod cli`
- src/core/mod.rs: drop `pub mod text_input_cli`
- src/core/cli.rs: update dispatch path
* refactor(tree_summarizer): move CLI adapter into domain
git mv src/core/tree_summarizer_cli.rs src/openhuman/tree_summarizer/cli.rs
and drop the stale `pub mod tree_summarizer_cli` from src/core/mod.rs.
Core CLI dispatcher now routes `tree-summarizer` to the domain-owned
adapter.
All five subcommands (ingest / run / query / status / rebuild) are short
-lived UX wrappers over registry handlers that already exist in
tree_summarizer/schemas.rs — no long-running server, no business logic
duplicated. Straight relocation keeps transport generic.
- git mv src/core/tree_summarizer_cli.rs src/openhuman/tree_summarizer/cli.rs
- src/openhuman/tree_summarizer/mod.rs: declare `pub mod cli`
- src/core/mod.rs: drop `pub mod tree_summarizer_cli`
- src/core/cli.rs: update dispatch path
* refactor(screen_intelligence): move + split CLI adapter into domain
Move src/core/screen_intelligence_cli.rs (699 lines, 7 subcommands) into
a domain-owned cli/ submodule, split by responsibility so no single file
exceeds the project's 500-line soft limit.
Layout:
screen_intelligence/cli/mod.rs (204) dispatch + shared helpers
(CliOpts, parse_opts,
bootstrap_engine, logging,
print_help)
screen_intelligence/cli/server.rs (79) run_server (long-running
capture+vision loop)
screen_intelligence/cli/session.rs (150) status / start / stop
screen_intelligence/cli/capture.rs (173) capture / vision (inspect pair)
screen_intelligence/cli/doctor.rs (107) readiness diagnostics
Classification before moving:
- no business-logic duplication vs schemas.rs handlers
- heavy fns (doctor 103, capture 97, start 89) were 80%+ pretty-printing
and CLI-side orchestration (polling loop, flag-driven save) — legit
CLI UX, not extracted domain logic
- shared helpers stayed pub(super) inside mod.rs; no new domain APIs
introduced
Core transport changes:
- delete src/core/screen_intelligence_cli.rs
- drop `pub mod screen_intelligence_cli` from src/core/mod.rs
- src/core/cli.rs dispatch routes to domain cli::
After this commit, src/core/cli.rs contains only dispatch lines for all
four migrated domains (voice, text_input, tree_summarizer,
screen_intelligence) — no embedded domain logic.
* refactor(screen_intelligence): address external review — extract capture save + tighten cli visibility
Follow-ups from codex + gemini review of the branch:
1. Extract CaptureFrame construction + disk save from the CLI.
`capture.rs --keep` was building a CaptureFrame inline (stamping
timestamps, copying context fields) and calling
`AccessibilityEngine::save_screenshot_to_disk` directly. Added
`AccessibilityEngine::save_capture_test_result(workspace_dir,
&CaptureTestResult, reason) -> Option<Result<PathBuf, String>>`
so the CLI only passes the result through and picks a reason
label. Domain owns the frame shape, not the CLI.
2. Tighten CLI module visibility across all four migrated domains.
`pub mod cli` -> `pub(crate) mod cli` and
`pub fn run_*_command` -> `pub(crate) fn` for voice, text_input,
tree_summarizer, screen_intelligence. Only src/core/cli.rs needs
to see these; they're transport plumbing, not domain public API.
No behavior change.
* style: cargo fmt
* address review comments from coderabbitai
- screen_intelligence/cli/server: use effective keep_screenshots (opts.keep
|| config flag) for both SiServerConfig and status output, so the server
behaves consistently with what the CLI reports.
- voice/cli: surface config load errors with a warning instead of silently
falling back to Config::default(); reject unknown --mode values instead
of silently coercing them to Push.
- PLAN.md: narrow the Phase 4 enforcement grep so it forbids domain
imports/non-dispatcher references in the transport layer while still
allowing crate::openhuman::<domain>::cli::run_*(...) dispatch calls,
which this PR legitimately introduces.
* style: cargo fmt voice cli
* style: cargo fmt + bump tauri-cef vendor
* fix(screen_intelligence): narrow save_capture_test_result to pub(crate) (addresses @coderabbitai nitpick on engine.rs:486)
AccessibilityEngine::save_capture_test_result is only called from the
domain's own cli/capture.rs (crate-internal). Making it pub leaks it
into the public API surface unnecessarily — tighten to pub(crate) to
match the visibility-tightening done across the rest of this PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* update
* chore: update .gitignore to include scheduled_tasks.lock in app/.claude
* ran formatter
---------
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
bc9e223306 |
feat(privacy): backend-backed capability privacy metadata + PrivacyPanel (#760)
* feat(about_app): add capability privacy metadata
Adds optional `CapabilityPrivacy { leaves_device, data_kind, destinations }`
to the about_app capability catalog so the in-app Privacy surface can be
backend-backed instead of hand-maintained. Twelve representative capabilities
are annotated for the first audited set (raw/local, derived/backend,
credentials, diagnostics, model download); remaining entries default to
None and are simply not surfaced. Wire format stays backward compatible
via skip_serializing_if.
* feat(settings): drive privacy panel from about_app capabilities
Replaces the hand-maintained privacy rows with data fetched from
openhuman.about_app_list. Only capabilities that ship privacy metadata
are rendered; loading and RPC failure both degrade gracefully and the
analytics toggle plus explanatory copy remain intact. Adds a small typed
client (utils/tauriCommands/aboutApp.ts) and focused vitest coverage for
render, omission of unannotated entries, and RPC failure.
---------
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
|
||
|
|
3bb714bf96 |
feat(webview): native OS notifications from embedded webview apps (#714) (#727)
* feat(webview_accounts): native OS notifications from embedded webviews (#714) Forward CEF notification intercept payloads to tauri-plugin-notification, prefixing the title with the provider label so the source of each toast is obvious at a glance. Honour `silent` (skip toast, still record route), `icon` (passed through to the native builder), and `tag` (used as the dedup key, with a monotonic timestamp fallback for untagged payloads). Record a NotificationRoute keyed by `{provider}:{account_id}:{tag_or_uuid}` so a future click hook (UNUserNotificationCenter / notify-rust on_response) can route the OS click back to the source account. Entries are cleared on webview_account_close / _purge to bound map growth. Expose webview_notification_permission_state / _request commands mapping tauri::plugin::PermissionState onto the web API triple. Non-cef stubs return "default" so the frontend can call the same invoke names on both runtimes. Wire notification:allow-* capabilities so the plugin can be invoked from the webview. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(accounts): wire notification permission + click bridge (#714) Round-trip the OS notification permission once per session on first account open via the new invoke pair. Attach a dormant notification:click listener that dispatches setActiveAccount and brings the main window to front when a platform click hook starts emitting the event — contract matches the Rust NotificationRoute shape so the emit side is a one-liner. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: sync Cargo.lock to 0.52.26 after version bump Lockfile picked up the pending 0.52.26 version bump from Cargo.toml while building the notification feature. No dependency graph change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(notifications): add notification bypass for embedded webview apps (#679) - Add NotificationBypassPrefs (global DND, per-account mute, bypass-when-focused) to WebviewAccountsState with thread-safe AtomicBool window focus tracking - Evaluate all three bypass conditions inside forward_native_notification before showing OS toast; each suppression path logs at debug with [notify-bypass] prefix - Add four new Tauri commands: webview_notification_set_dnd, webview_notification_mute_account, webview_notification_get_bypass_prefs, webview_set_focused_account - Wire window focus tracking in setup hook via on_window_event Focused handler - Frontend: add setAccountMuted, setGlobalDnd, getBypassPrefs, setFocusedAccount helpers in webviewAccountService; sync focused account on open + click - Add NotificationsPanel settings page with Global DND toggle - Register NotificationsPanel at /settings/notifications Closes #679 * feat(notifications): integrate notifications feature into app - Added Notifications page and routing to AppRoutes. - Introduced NotificationRoutingPanel in Settings for managing notification settings. - Updated SettingsHome to include navigation for notification routing. - Integrated notifications reducer into the store for state management. - Enhanced Rust backend to support notification handling from embedded webviews. This commit lays the groundwork for a comprehensive notification system within the application. * refactor(notifications): clean up code formatting and structure - Simplified JSX structure in NotificationCard for better readability. - Consolidated fetchNotifications call in NotificationCenter for cleaner syntax. - Improved formatting in NotificationRoutingPanel and notificationsSlice for consistency. - Enhanced Rust code readability by streamlining function signatures and logic. These changes enhance code maintainability and readability across the notifications feature. * refactor(webview_accounts): simplify webview_notification_set_dnd function signature - Removed unnecessary line breaks in the webview_notification_set_dnd function for improved readability. * feat(notifications): implement provider-level notification settings management - Added `getNotificationSettings` and `setNotificationSettings` functions to manage notification settings for providers. - Enhanced `NotificationRoutingPanel` to display and update settings for Gmail, Slack, Discord, and WhatsApp. - Introduced new RPC endpoints for retrieving and updating notification settings. - Updated database schema to store notification settings persistently. This commit establishes a robust system for managing notification preferences, improving user control over notifications. * refactor(notifications): improve code formatting and readability - Enhanced formatting in NotificationRoutingPanel for better clarity. - Streamlined function signatures in notificationService and Rust backend. - Improved readability of assertions in tests by adjusting line breaks. These changes contribute to a more maintainable and comprehensible codebase for the notifications feature. * chore(vendor): bump tauri-cef to fix Slack notification permission banner Updates the tauri-cef submodule to 55db2d6 which adds a navigator.permissions.query shim in the CEF render process. Slack checks this API (not just Notification.permission) to decide whether to show its "needs your permission" banner — the shim returns "granted" for notifications queries so the banner no longer appears. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(vendor): bump tauri-cef for cargo fmt fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(vendor): bump tauri-cef — native V8 permissions.query shim Switches from context.eval() to a proper PermissionsQueryV8Handler so the navigator.permissions.query fix actually runs in on_context_created. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(notifications): patch navigator.permissions.query in ua_spoof.js The V8 set_value_bykey approach in cef-helper's on_context_created does not stick on CEF platform objects (Chromium's V8 binding layer silently ignores property writes on native wrappers like Permissions). The init script path via frame.execute_java_script runs in the fully-initialised JS context where navigator.permissions IS writable, matching how ua_spoof.js already overrides navigator.userAgent successfully. Slack checks navigator.permissions.query({ name: 'notifications' }) before showing its "needs permission" banner — patching it here to return "granted" removes the banner. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(notifications): use Object.defineProperty to shim navigator.permissions Two-layer fix for the Slack "needs permission to enable notifications" banner: 1. cef-helper (submodule update to 99a2686): context.eval() in on_context_created installs Object.defineProperty(navigator, 'permissions', ...) before any page JS runs. 2. ua_spoof.js: same Object.defineProperty pattern as belt-and-suspenders for frames that reload or trigger permission checks after on_load_end. Simple property assignment on Blink platform objects is silently ignored; Object.defineProperty on the navigator wrapper itself (the same mechanism already used for navigator.userAgent) works correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(notifications): address CodeRabbit review issues on PR #727 - Move raw_title out of log::info! (PII risk) — log title_chars at info, raw_title at debug only - Fix permissionChecked set before async invoke in ensureNotificationPermission so transient failures allow retry on next account open * fix(cef): enable webview-data-url feature for CEF placeholder URL The CEF backend uses a data: URL as the initial webview location so CDP can attach before the real provider URL loads. Tauri's add_child rejects data: URLs unless the webview-data-url feature is enabled. * fix(notifications): address remaining CodeRabbit issues on PR #727 - cdp/emulation: bump Chrome UA 124→136 to pass Slack browser check - cdp/session: inject Page.addScriptToEvaluateOnNewDocument to stub Notification.permission as "granted" and silence provider banners - notifications/mod.rs + core/all.rs: wire notifications domain into the controller registry (fixes unknown-method in json_rpc_e2e tests) - notifications/schemas: add skipped bool output to ingest schema - notifications/store: add tracing::warn on datetime parse failure - notificationService: union return type for ingestNotification - webviewAccountService: narrow union before accessing result.id - NotificationCenter: drive loading/error from fetch effect; track allProviders separately so filter pills don't collapse on selection - NotificationRoutingPanel: rollback optimistic update on save failure - useSettingsNavigation: add notifications/notification-routing routes - scripts/install.sh: remove silent dry-run exit 0 on asset failure - scripts/setup-dev-codesign.sh: remove unconditional -legacy flag - docs/SUMMARY.md: remove worktree path, fix macOS capitalisation, remove self-referential deletion note * chore: apply prettier + cargo fmt + fix useEffect dep warning Auto-apply formatting changes flagged by the pre-push hook: - prettier reformatted NotificationCenter.tsx and notificationService.ts - cargo fmt reformatted all.rs, openhuman/mod.rs, notifications/schemas.rs - NotificationRoutingPanel: move providers array to module scope so useEffect dependency array is satisfied without exhaustive-deps warning * feat(notifications): enhance notification management and permissions - Added new commands for managing notification preferences, including setting global Do Not Disturb (DND), muting specific accounts, and retrieving current bypass preferences. - Implemented a notification permission state handler to ensure consistent behavior across different environments. - Updated the JavaScript shim for notification permissions to handle both Notification and PushManager states, ensuring compatibility with various providers. - Refactored the WebviewAccountsState to include a new structure for managing notification bypass preferences, improving the overall notification handling logic. * update agents * fix(notifications): complete schema + navigation metadata for ingest/settings routes - app/src/components/settings/hooks/useSettingsNavigation.ts: resolve the new `/settings/notifications` and `/settings/notification-routing` URLs to their SettingsRoute values and feed them into breadcrumbs so the new panels don't silently fall through to `'home'`. Addresses CodeRabbit on useSettingsNavigation.ts:34. - src/openhuman/notifications/schemas.rs: add the optional `reason` output on `notification.ingest` (populated alongside `skipped=true` by the runtime) and the normalized `settings` output on `notification.settings_set` so schema-driven clients see the full response shape. Addresses CodeRabbit on schemas.rs:103 and schemas.rs:217. - src/core/all.rs: add a `notification` namespace_description so CLI help covers the new controllers, plus a test assertion. Addresses CodeRabbit on src/core/all.rs:149. * fix(notifications): trace DB entry, surface empty update matches, warn on bad scored_at - Add `tracing::trace!` checkpoints around the `with_connection` DB open and schema migration so notification-delivery issues are reconstructible from logs. - `update_triage` and `mark_read` now inspect `Connection::execute`'s affected-row count: log a `warn!` when the update matched zero rows (row deleted between ingest and scoring / client passed a stale id), `debug!` on the normal path. - `scored_at` parsing no longer silently drops malformed values — log a `warn!` with the raw value and parse error before treating the row as unscored, matching the existing behavior for `received_at`. Addresses CodeRabbit on store.rs (lines 72, 172, 294). * fix(webview): respect silent notifications, multi-host CDP fallback, shim idempotency - webview_accounts/mod.rs: honor the Web Notification `silent` flag. Previously we only logged it and still called `builder.show()`, so pages that marked a notification silent still produced an OS toast. Mirror event still fires so the in-app center updates; only the OS toast is suppressed. Also picks up a prior cargo-fmt rewrap. - cdp/target.rs: `browser_ws_url()` now continues the host loop when `resp.json()` fails instead of early-returning via `?`. A malformed response from the first host (CDP_HOST) no longer prevents the `localhost` fallback from being tried. - webview_accounts/ua_spoof.js: guard the Notification wrapper behind `window.__OH_NOTIF_SHIM` so repeated evaluations of the script (Page.addScriptToEvaluateOnNewDocument + frame-level re-injections) don't stack wrappers onto the same page globals or re-proxy `Function.prototype.toString`. Addresses CodeRabbit on webview_accounts/mod.rs:377, cdp/target.rs:33, and ua_spoof.js:176. * update agents * update --------- Co-authored-by: oxoxDev <nikhil@tinyhumans.ai> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
1461d78509 |
feat(composio): inject connected identities into agent prompts (#774)
* chore(vendor): bump tauri-cef to feat/cef tip (073047817) * feat(composio): inject connected identities into agent prompts Standardize provider identity persistence with a shared identity_set hook and skill-scoped facet keys so connected account identity survives restarts and is merged into inference context. Add connected identity rendering in welcome, orchestrator, and integrations_agent prompts so agents can reference cross-platform user identity during conversations. Closes #691 Made-with: Cursor * fix(composio): resolve CodeRabbit identity prompt and cleanup findings Make connected-identity prompt injection deterministic via PromptContext, sanitize provider identity fields before prompt rendering, and clear persisted identity facets when Composio connections are removed to avoid stale context. Made-with: Cursor * fix(ci): satisfy format checks and dry-run installer smoke Apply rustfmt to touched Rust files and make install.sh dry-run exit successfully when no compatible release asset exists, so smoke validation remains informative without failing on missing artifacts. Made-with: Cursor |
||
|
|
581f37965c |
feat(skills): SKILL.md skills UI — browse, create, install from URL (#681) (#740)
* feat(skills/core): add read_skill_resource with size + traversal guards (#681) Introduces `read_skill_resource(skill_id, relative_path)` in the skills ops module. Used by the new `skills.read_resource` RPC (landed in a follow-up commit) to let the UI preview files bundled alongside a SKILL.md without having to shell out to the Node runtime. Guards rejecting each known attack surface have their own unit test: - empty skill_id / empty relative_path - unknown skill - absolute paths - `..` traversal escapes (checked after canonicalization against the skill root, reusing the pattern from the Node exec allowlist) - directory targets - symlinked leaves (reject via `symlink_metadata` before open) - files over the 128 KB cap - non-UTF-8 content (binary allowlist is text-only) Happy-path test covers a small text resource under the skill root. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(skills/core): wire skills.read_resource RPC + namespace (#681) Adds `skills` RPC namespace with `skills.list` and `skills.read_resource` handlers. `read_resource` delegates to `read_skill_resource` (previous commit) and surfaces path-traversal / size / encoding errors back to the caller verbatim so the UI can render the error string as-is. - `src/openhuman/skills/schemas.rs` (new): controller + schema definitions, plus unit tests for schema name stability, round-trip of the minimum `SkillSummary` fields, and controller list/schema length parity. - `src/openhuman/skills/mod.rs`: declare `pub mod schemas` and re-export `all_skills_controller_schemas`, `all_skills_registered_controllers`, and `skills_schemas`. - `src/core/all.rs`: register the controllers + schemas and add a namespace description so the RPC discovery endpoint surfaces it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(app/api): typed skillsApi client for list + read_resource (#681) Thin typed wrapper around the `skills.list` and `skills.read_resource` RPCs added in the previous commit. The client normalises the backend response shape (bytes + UTF-8 content) and rethrows backend error strings verbatim so the preview pane can render them unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(app/ui): SkillResourceTree groups bundled resources by top dir (#681) Presentational component that takes the `resources: PathBuf[]` from a loaded Skill and renders it as a grouped list (scripts, assets, references, etc. based on the first path segment). Selecting a leaf calls `onSelect(relativePath)` so the parent drawer can drive preview state. Stateless — no fetching, no effects. Styling follows the stone/coral design tokens used across the Skills page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(app/ui): SkillResourcePreview size-gated viewer + specs (#681) Presentational component that fetches a single bundled resource via `skillsApi.readSkillResource`. Backend caps payloads at 128 KB and either returns UTF-8 text or a plain error string, so the preview pane has three visual states: loading, error, success. - On error (e.g. "path escape", ">128KB", "non-UTF-8"), renders the backend message verbatim in a coral panel. - On success, renders a monospace pre block with the byte count in the footer. - `key={id:path}` on the mount site (in SkillDetailDrawer, next commit) drives a remount when the selected resource changes — so no setState-in-effect hack is needed to reset loading state. Vitest specs cover: loading state, success rendering with byte footer, error rendering for traversal / oversize / encoding strings, cancelled fetch guard on unmount. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(app/ui): SkillDetailDrawer right-side detail panel + specs (#681) Slide-in right-hand drawer that displays frontmatter metadata (description, version, author, license, tags, allowed_tools) and hosts SkillResourceTree + SkillResourcePreview. Opened by clicking a skill card on the Skills page (wired in the next commit). - Focus management: on mount, focuses the close button via `window.requestAnimationFrame` and restores the previously focused element on unmount. - Esc + backdrop click dismiss. - Preview pane is conditionally rendered and keyed on `${skill.id}:${selectedResource}` so changing the selected resource remounts the previewer (avoids setState-in-effect pattern). Vitest specs cover: render with frontmatter, resource tree click opens preview, close button / Esc / backdrop dismiss paths, focus restoration, empty-resources case. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(app/ui): open SkillDetailDrawer on skill card click (#681) Wires the Skills page to the new drawer: clicking a skill card sets `selectedSkill` state, which mounts `SkillDetailDrawer`. Dismissing the drawer clears the state. Cards gain an explicit "View details" affordance for discoverability. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(skills/core): add skills.create RPC for scaffolded SKILL.md authoring (#681) Adds `create_skill` in ops.rs plus the `skills.create` controller + handler in schemas.rs. Writes a minimal SKILL.md (with optional license/author/tags/ allowed-tools frontmatter) under the selected scope, scaffolds scripts/ references/assets subdirs, and re-discovers the skill to return the parsed SkillSummary. Legacy scope rejects; Project scope requires the trust marker; User scope is always allowed when a home directory is available. Hardened against path traversal the same way read_skill_resource is: canonicalize the scope root, canonicalize the target dir, reject unless the target starts with the root. Slug derivation is ASCII-only (collapse whitespace/ -/_ to a single hyphen, drop other chars, trim hyphens, enforce MAX_NAME_LEN). Tests (hermetic via create_skill_inner): - user-scope happy path (slug, metadata, SKILL.md on disk, subdirs) - slug collision rejection - invalid name (no alphanumerics) rejection - project-scope without trust marker rejection - project-scope with trust marker happy path - legacy-scope rejection - empty-description rejection - slugify edge cases Closes part of #681 (backend scope for create flow). * feat(skills/core): add skills.install_from_url RPC via npx skills add (#681) Introduces `install_skill_from_url(url, timeout_secs?)` — a JSON-RPC method that shells out to `npx --yes skills add <url>` under the managed Node runtime so the UI can install published SKILL.md packages directly. Security posture: - https scheme only (no http, file, ssh, git+https…) - Rejects `localhost`, `*.localhost`, `*.local`, RFC1918 private IPv4, loopback, link-local, multicast, broadcast, unspecified, 100.64/10 CGN, 0.0.0.0/8, and IPv6 loopback/unspecified/multicast, fc00::/7 ULA, fe80::/10 link-local. Explicitly covers 169.254.169.254 cloud metadata. - Trims + caps URL at 2048 chars; parses with the `url` crate. - IPv6 brackets stripped from `host_str()` before address parse. Process posture: - Reuses `NodeBootstrap` so the managed toolchain resolves first. - `env_clear()` + explicit PATH injection (bootstrap bin_dir first) + a narrow safe-env allow-list (HOME, TERM, LANG, LC_ALL, LC_CTYPE, USER, SHELL, TMPDIR). Matches the npm_exec pattern from #723. - Default 60s wall-clock timeout, capped at 600s. - Captures stdout/stderr; returns both on success or failure. - Diff-based `new_skills`: snapshots discovered skills pre-install and reports slugs that appear post-install. Surface: - JSON-RPC: `openhuman.skills_install_from_url` params: { url: string, timeout_secs?: number } result: { url, stdout, stderr, new_skills[] } - Wired into `all_skills_controller_schemas()` and `all_skills_registered_controllers()`. Tests (5 new unit tests, all pass — 51/51 in skills::): - validate_install_url_accepts_public_https - validate_install_url_rejects_non_https_scheme (http, file, ftp, ssh, git+https, javascript) - validate_install_url_rejects_empty_and_oversized - validate_install_url_rejects_private_and_loopback (20 URLs inc. CGN, cloud metadata, IPv6 ULA/link-local/loopback/multicast) - validate_install_url_rejects_malformed (missing scheme, empty host, non-https scheme, unparseable bracketed host) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(app/api): skillsApi.createSkill + installSkillFromUrl wrappers (#681) Adds typed frontend wrappers for the two new skill-authoring RPC methods: - `skillsApi.createSkill(input)` — scaffolds a new SKILL.md skill via `openhuman.skills_create`. Accepts camelCase `allowedTools` and rekeys it to the `allowed-tools` spelling the SKILL.md frontmatter convention expects, matching `SkillsCreateParams` in `src/openhuman/skills/schemas.rs`. Optional fields are only sent when explicitly provided so the Rust `#[serde(default)]` defaults apply cleanly. - `skillsApi.installSkillFromUrl(input)` — installs a published skill package via `openhuman.skills_install_from_url`. Accepts camelCase `timeoutSecs` and rekeys it to `timeout_secs`. Normalizes the response (snake_case `new_skills` -> camelCase `newSkills`, missing list -> []). Both wrappers reuse the existing `unwrapEnvelope` helper so they tolerate either a bare RPC payload or the `{ data: … }` envelope some transports emit. Adds `CreateSkillInput`, `InstallSkillFromUrlInput`, and `InstallSkillFromUrlResult` type exports for downstream modal components. Tests (vitest, 6 new specs, all pass): - createSkill forwards inputs and rekeys allowedTools - createSkill omits optional fields when absent - createSkill unwraps envelope responses - installSkillFromUrl forwards url and rekeys timeoutSecs - installSkillFromUrl omits timeout_secs + defaults newSkills to [] - installSkillFromUrl unwraps envelope responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(app/ui): CreateSkillModal for scaffolding SKILL.md skills (#681) Adds a centered white modal that scaffolds a new SKILL.md skill via `skillsApi.createSkill`, matching the settings-modal design rules (520px desktop, 16px radius, backdrop+blur, Escape/click-out to close, focus capture). Form fields mirror the Rust `SkillsCreateParams` schema: - name (required) — display name, also slugified into the on-disk directory; a live slug preview surfaces what will hit disk - description (required) — short prose; written as the `description:` field in the generated YAML frontmatter - scope (user | project radio) — `legacy` is hidden because that layout is read-only and being phased out - license (optional) — free-form SPDX-style string - author (optional) - tags (optional, CSV) — normalised client-side; empty entries dropped - allowedTools (optional, CSV) — rekeyed to `allowed-tools` on the JSON-RPC wire by `skillsApi.createSkill` The slug preview mirrors `slugify_skill_name` on the Rust side (lowercase ASCII alnum + `-`, collapse repeats, trim edge hyphens) so the user sees what the Rust slugifier will produce; the Rust side stays authoritative when the skill is persisted. On success `onCreated(skill)` fires with the freshly-discovered `SkillSummary`, letting the parent grid insert the new row without a full refetch. On failure the Rust error string is surfaced verbatim in a coral-styled alert and the submit button re-enables. Vitest specs cover: required-field rendering, live slug preview, submit-disabled gating, Escape close, wire-format rekey of `allowedTools` → `'allowed-tools'`, `onCreated` dispatch, and error-banner recovery. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(app/ui): InstallSkillDialog for npx skills add <url> (#681) Adds a centered white modal that installs a published skill package via `skillsApi.installSkillFromUrl`. The Rust side shells out to `npx --yes skills add <url>` under the managed Node toolchain, with an allow-list on the URL (https only, no private/loopback/link-local/ multicast/cloud-metadata hosts) and a wall-clock timeout (default 60s, max 600s). UI contract: - Single URL input plus optional timeout in seconds. - Client-side `isLikelyValidUrl` fails fast on non-https URLs so the user doesn't pay a round-trip for shape errors the Rust side would reject anyway; the Rust side remains authoritative. - Timeout field validates `1 <= n <= 600` client-side to mirror the server-side clamp range. - While the RPC is in flight we render a spinner with "Running `npx skills add`…" copy and disable close / backdrop dismiss so we don't orphan the subprocess. - On success we surface the list of `newSkills` (ids that appeared post-install) plus captured stdout/stderr panes inside collapsible <details> elements, then hand the full result back to the caller via `onInstalled` so the parent can refetch the skills list and auto-select the new row. - On failure the Rust error string is rendered verbatim in a coral alert and the submit button re-enables. Vitest specs cover: required-field rendering, URL shape gating (empty, malformed, http://, https://), timeout range validation, `timeoutSecs` forwarding on submit, success panel with newSkills rendering, blank timeout omitted from payload, and error-banner recovery. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(app/ui): wire New skill + Install from URL into Skills page (#681) Adds a header row on the Skills page with two buttons: - **New skill** → opens `CreateSkillModal` - **Install from URL** → opens `InstallSkillDialog` Extracts the existing `listSkills` effect into a reusable `refreshDiscoveredSkills` helper so both new flows can reconcile their results against the freshly-discovered `SkillSummary` rows rather than relying on the optimistic payload from the RPC alone. Create flow: - Optimistically appends the returned `SkillSummary` to `discoveredSkills` (dedupe by id). - Auto-opens the detail drawer for the new skill so the user lands in context — matches the install flow's UX. - Follows up with `refreshDiscoveredSkills()` so version/author/ warnings picked up by the Rust discoverer end up in state too. Install flow: - Always refreshes the list (the install can add multiple skills if the package declares several). - Auto-opens the detail drawer for the first newly-installed skill when at least one id is reported back; otherwise leaves the grid in its refreshed state. Both buttons sit in a flush `max-w-lg` header above the existing search bar, styled consistent with `UnifiedSkillCard` CTAs — ocean primary for "New skill" (positive action), neutral stone for "Install from URL" (secondary). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(skills/core): direct SKILL.md fetch replaces npx skills add (#681) Installer no longer shells out to the vercel-labs/skills CLI. It now fetches SKILL.md over HTTPS, validates YAML frontmatter, and writes into the user's skills dir. Size cap (1 MiB), timeout clamp (1-600s), GitHub blob->raw URL normalization, and path-traversal guards are covered by unit tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(app/ui): install dialog copy + categorized errors for direct fetch (#681) Dialog subtitle, helper text, and in-flight indicator reflect the new direct SKILL.md fetch flow. Errors from the core are categorized into friendly titles (URL rejected, too large, timeout, parse failure, already installed, write failed) with the raw backend message tucked under a details disclosure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(app/ui): dialog specs cover direct-fetch fixtures + error categorization (#681) Fixtures updated to raw GitHub SKILL.md URLs. New cases assert the categorization helper surfaces the right title for invalid SKILL.md, unsupported URL form, and unknown backend errors (raw text hidden under details). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(skills/core): install_from_url writes to user scope (#681) Project scope (`<ws>/.openhuman/skills/`) is gated on a `<ws>/.openhuman/trust` marker that the workspace rarely has, so freshly-installed skills were invisible to `skills.list` until the user opted the workspace into trust. Route installs to `~/.openhuman/skills/<slug>` — the user-scope root that `discover_skills` always scans — so "Install from URL" surfaces the new skill immediately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(skills): align install_from_url docs with direct-fetch impl (#681) `install_from_url` stopped shelling out to `npx --yes skills add <url>` when it was rewritten to fetch SKILL.md over HTTPS directly, but schema descriptions and SDK wrappers still described the old subprocess flow. Fix the module-level rustdoc, the JSON-RPC schema `description`/`comment` fields, and the TS client wrapper doc comments so the surface documents what actually runs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(skills/core): DNS-to-private-IP SSRF guard + install rollback (#681) Two fixes surfaced by CodeRabbit review on PR #740: * `validate_install_url` only inspected literal-IP hosts, so a public-looking hostname like `evil.example.com` with an A record pointing at `127.0.0.1` / `169.254.x` / etc. would still be handed to `reqwest`. Resolve the host via `tokio::net::lookup_host` before the GET and reject if any returned address falls in loopback / private / link-local / multicast / unspecified ranges. Document the remaining DNS-rebinding gap (pinning to a `SocketAddr` + custom reqwest resolver is tracked separately). * If `std::fs::write` or `std::fs::rename` fails after `create_dir_all` succeeded, the empty/partial target directory used to survive and permanently block retries under the same slug. Wrap the write+rename in a rollback that removes the temp file + the just-created directory on failure (best-effort; cleanup errors are logged and the original write error is surfaced). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style(skills): cargo fmt break long Host::Ipv6 conditional (#681) CI's cargo fmt (stable) rewraps the long `.map(..).unwrap_or(false)` chain that passed local fmt. Apply the break so the pre-push hook and the upstream lint job agree. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
32b5afd1d9 |
feat(memory): topic trees (lazy hotness-driven materialisation) (#709) (#799)
Phase 3c of the memory architecture (umbrella #711). Adds per-entity topic trees spawned lazily when an entity's hotness crosses a threshold, so Phase 4 retrieval can resolve "what did Alice say about Phoenix?" without scanning the full chunk pool. ## What's in this commit New module `src/openhuman/memory/tree/topic_tree/`: - `types.rs` — `EntityIndexStats` (hotness input), `HotnessCounters` (the persisted row) and the `TOPIC_CREATION_THRESHOLD=10.0`, `TOPIC_ARCHIVE_THRESHOLD=2.0`, `TOPIC_RECHECK_EVERY=100` constants. - `hotness.rs` — pure arithmetic scorer plus deterministic `hotness_at(entity_id, stats, now_ms)` variant for tests, and a piecewise-linear `recency_decay` helper. - `store.rs` — SQLite helpers (`get`, `get_or_fresh`, `upsert`, `distinct_sources_for`, `count`) for the new `mem_tree_entity_hotness` table. - `registry.rs` — `get_or_create_topic_tree`, `force_create_topic_tree`, `list_topic_trees`, `archive_topic_tree`. Race-recovers on UNIQUE violations via `is_unique_violation`, mirroring `source_tree::registry`. - `curator.rs` — `maybe_spawn_topic_tree` (per-ingest tick) and `force_recompute` (admin path). Implements the `TOPIC_RECHECK_EVERY`-gated recompute: refresh `distinct_sources` from the entity index, compute hotness, spawn + backfill if threshold crossed. - `backfill.rs` — `backfill_topic_tree` walks the entity index and routes every historic leaf (ts-ASC ordered) through `source_tree::bucket_seal::append_leaf`. Capped at 500 leaves. Skips summary-node hits so topic trees only ever ingest raw leaves. Missing chunks log a warn and are skipped, never failing the spawn. - `routing.rs` — `route_leaf_to_topic_trees` fans a kept leaf out to every active matching topic tree and ticks the curator for each entity. Archived trees are skipped but counters still bump. Wired from ingest (`tree/ingest.rs::append_leaves_to_tree`) AFTER the source-tree `append_leaf` succeeds — non-fatal on error, logged at warn level so routing issues never poison the ingest hot path. ## Hotness scoring (pure) ``` hotness = ln(mentions + 1) // dampened volume + 0.5 * distinct_sources // cross-source bonus + recency_decay(last_seen) // 1.0 @ day 0 → 0 @ day 30 + graph_centrality // Phase 4+; None → 0 + 2.0 * query_hits // retrieval feedback; Phase 4+ ``` `graph_centrality` and `query_hits_30d` columns are persisted but the increment code paths are deferred to later phases, as planned. ## Schema (additive, idempotent) New table `mem_tree_entity_hotness` (keyed on `entity_id`) added to the Phase 1 `SCHEMA` constant in `tree/store.rs` so it migrates through the same `with_connection` path as the existing tables. `CREATE TABLE IF NOT EXISTS` keeps it re-run-safe. An ancillary index on `last_hotness` supports future sweep queries. ## Routing The ingest path is the only non-admin caller. After `append_leaf` puts a leaf in the source tree, `route_leaf_to_topic_trees` runs with the chunk's canonical entity list. For each entity: 1. If an active topic tree exists, append the leaf to it (reusing `source_tree::bucket_seal::append_leaf` with `entities=[entity_id]`). 2. Tick `maybe_spawn_topic_tree` — may bump counters or, on cadence, recompute hotness and spawn + backfill a new tree. Per-entity errors are caught and logged; a top-level failure is demoted to a warn in the ingest caller so the source-tree append always wins. ## Reuses from Phase 3a - `source_tree::bucket_seal::append_leaf` — same `&Tree` API works for `TreeKind::Topic` end-to-end. - `source_tree::summariser::{Summariser, InertSummariser}` — honest stub emits empty entity/topic vecs on summary nodes. - `mem_tree_trees` / `mem_tree_summaries` / `mem_tree_buffers` schema, discriminated by `kind = 'topic'` and `scope = <entity canonical id>`. - Registry race recovery via `is_unique_violation` (catches UNIQUE on `insert_tree`, re-queries on collision). - Idempotent append (duplicate `item_ids` in the L0 buffer are no-ops), so backfill is safe to re-run. ## Tests 40 new tests (10 hotness, 3 types, 6 store, 7 registry, 4 curator, 4 backfill, 5 routing, 1 integration). `memory::tree` suite: 201 passing, 0 regressions (Phase 3a baseline was 161 → +40 new). Coverage highlights: - Hotness pure math (zero-entity, spike-over-threshold, old-but-widely- cited retains signal, query-hit boost, recency decay edges). - Curator: first-ingest-just-bumps, no-spawn-below-threshold, spawn-fires-exactly-once-when-crossed, cadence gating. - Backfill: appends-all-entity-leaves, skips missing chunks, idempotent, skips summary nodes. - Routing: empty-entities-noop, appends-to-existing-tree, archived-tree- skipped, multi-entity-fan-out, end-to-end-integration-materialisation. - Registry: idempotent get-or-create, UNIQUE race recovery, archive flips status (not deletion), kind/scope cleanly separated from source. ## Deliberate non-goals (deferred) - No JSON-RPC surface (out of scope for Phase 3c core). - No archive cron sweep — `archive_topic_tree` primitive only. - `graph_centrality` / `query_hits_30d` increments deferred to later phases (columns exist, reads work, writes are Phase 4+). ## Stacked on #789 Base is `feat/709-summary-trees`. Merge #789 first, then rebase this branch. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
48897af9b3 |
feat(memory): global activity digest tree (#709) (#798)
Phase 3b of the memory architecture (umbrella #711). Adds a singleton cross-source `global` tree whose purpose is time-windowed recap ("what did I do in the last 7 days?") via a time-axis-aligned hierarchy: L0 = day, L1 = week (7 dailies), L2 = month (4 weeklies), L3 = year (12 monthlies). ## What's in this PR - `global_tree/registry.rs` — singleton `get_or_create_global_tree` with the same UNIQUE-race recovery pattern Phase 3a uses for source trees (scope is the literal `"global"`). - `global_tree/digest.rs` — `end_of_day_digest(config, day, summariser)` walks every active source tree, picks one representative contribution per tree (latest L1+ intersecting the day, fallback to root), folds them with the Summariser trait into one L0 daily node, inserts it into `mem_tree_summaries`, and triggers the L0→L1→L2→L3 cascade. Idempotent on re-run (returns `DigestOutcome::Skipped` when an L0 already exists for the day). - `global_tree/seal.rs` — count-based cascade-seal with thresholds 7 (weekly), 4 (monthly), 12 (yearly). Transactional append with idempotency on (tree_id, level, item_id) to survive partial retries. - `global_tree/recap.rs` — `recap(config, window)` maps the window to a level (<2d→L0, <14d→L1, <60d→L2, else L3), fetches covering summaries, and falls back downward when the chosen level has no sealed material yet (reports `level_used` so callers can surface "best available"). Empty tree returns `None`. - `source_tree/store.rs` — adds `list_trees_by_kind` used by the digest to enumerate source trees. - `tree/mod.rs` — exports the new `global_tree` module. ## Reuses from Phase 3a - `source_tree::store` CRUD for `mem_tree_trees` / `mem_tree_summaries` / `mem_tree_buffers` (no new schema tables). - `source_tree::summariser::{Summariser, SummaryContext, SummaryInput, InertSummariser}` — honest-stub entity/topic semantics kept as-is. - `source_tree::registry::new_summary_id` for id generation. - `source_tree::types::{Tree, SummaryNode, Buffer, TreeKind::Global, TreeStatus}` — `TreeKind::Global` was already declared in Phase 3a. - Entity backfill via `tree::score::store::index_summary_entity_ids_tx` so Phase 4 retrieval can resolve "summaries mentioning X" through the same inverted index as leaves. ## Design decision: seal.rs kept as parallel impl `source_tree::bucket_seal::cascade_all_from` uses a token-budget seal policy (`TOKEN_BUDGET`), not pluggable. The global tree needs count-based thresholds per level. Rather than refactoring the stable Phase 3a seal pipeline to accept a policy (regression risk), we keep `global_tree/seal.rs` as a parallel count-based implementation that routes through the same `source_tree::store` primitives. The shape of the transaction is intentionally identical so future consolidation is straightforward when we're ready to touch the Phase 3a seal path. ## Tests 15 new tests; 176 total `memory::tree::*` passing (was 161). Coverage: - registry idempotency, ID prefix, UNIQUE-race recovery - digest empty-day, populated-day, rerun idempotency, 7-day weekly cascade - seal below-threshold, weekly-threshold, append idempotency - recap level selection across all four bands, empty-tree, L0 fallback, L1 when sealed ## Stacked on #789 Base is `feat/709-summary-trees` (Phase 3a). Merge #789 first, then rebase this branch. PR body should flag the stack order. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
37a73df0c0 |
feat(memory): source trees + bucket-seal foundation (#709) (#789)
* feat(memory): LLM-based NER + importance signal with cheap-signals short-circuit (#708)
Adds an Ollama-backed entity extractor and an LLM-derived importance
score as a new signal in the admission gate. Builds on merged Phase 2
(#733) without changing its surface — additive only.
Why LLM (not GLiNER): zero new deps (reuses existing reqwest + async
infra), reuses the same Ollama setup openhuman uses for embeddings,
better per-entity quality scales with model choice, no native ONNX
runtime to ship. Latency cost (~100-300ms per chunk on a small model
like qwen2.5:0.5b) is amortised by the short-circuit.
The admission gate stays a hybrid - cheap deterministic signals
always run; the LLM is one signal among many, never the sole arbiter.
Backwards-compatible: with default SignalWeights the LLM weight is 0.0
and behaviour matches pre-LLM Phase 2 exactly.
What's new:
- src/openhuman/memory/tree/score/extract/llm.rs: LlmEntityExtractor
implementing the existing EntityExtractor trait. Posts a structured
JSON request to an Ollama-compatible /api/chat endpoint asking for
NER + an importance rating in one call. Span recovery via string
search; hallucinated entities (surface not in source text) dropped.
Soft fallback: HTTP failures log a warn and return empty extraction.
- ExtractedEntities gains llm_importance (Option<f32>) +
llm_importance_reason (Option<String>); merge() takes max importance.
- ScoreSignals gains llm_importance (f32, defaults to 0.0).
- SignalWeights gains llm_importance (default 0.0; with_llm_enabled()
helper sets it to 2.0 - comparable to metadata/source weights).
- signals::combine_cheap_only(): variant that excludes the LLM signal,
used for the short-circuit decision in score_chunk.
- ScoringConfig gains llm_extractor (Option<Arc<dyn EntityExtractor>>),
definite_keep_threshold (default 0.85), definite_drop_threshold
(default 0.15). New with_llm_extractor() constructor.
- score_chunk() pipeline:
1. Always-on regex extraction
2. Cheap signals + combine_cheap_only -> cheap_total
3. If cheap_total >= definite_keep OR <= definite_drop: skip LLM
4. Else (borderline band): run LLM extractor, merge results, recompute
5. Final combine + admission gate against drop_threshold
- mem_tree_score table gains nullable llm_importance + llm_importance_reason
columns via idempotent ALTER TABLE migration.
- ScoreRow + upsert/get persist the new column.
What's not changed:
- Existing JSON-RPC surface
- Default behaviour (LLM weight=0, no llm_extractor configured = same
outputs as before)
- mem_tree_chunks / mem_tree_entity_index schemas
Tests added:
- LLM extractor: prompt construction, JSON parsing, hallucination
drop, importance clamping, strict vs lenient unknown-kind handling
- Score pipeline: short-circuit on definite_keep/definite_drop skips
LLM call (verified via FakeLlm call counter); borderline band
consults LLM once; LLM failure falls back gracefully without erroring
LLM-NER work follows up on #708. Parent: #711.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(memory): address CI failures and CodeRabbit review on LLM-NER (#708)
CI was red on PR #775 due to 10 E0063 errors — existing test-only struct
literals for ExtractedEntities / ScoreSignals / ScoreRow weren't updated
when the new optional llm_importance fields landed. Local cargo check
on lib-only passed; cargo check --tests caught them, as does CI.
CodeRabbit also flagged four semantic issues:
1. LlmEntityExtractor::extract() returned Err on HTTP transport, non-2xx
status, and JSON parse failures. The soft-fallback contract (documented
in the module header) only worked because score_chunk catches errors;
any other caller got a hard error. Now split into extract_or_empty()
which logs a warn on each failure mode and returns
ExtractedEntities::default() unconditionally. The public extract()
stays on the async-trait surface but never returns Err.
2. find_char_span() always searched from byte offset 0, so when the LLM
returned the same surface twice (explicitly asked for duplicates in
the prompt), both entities got (start=0, end=len) — same span, not
distinct mentions. Added find_char_span_from(haystack, needle,
byte_from, char_from) that resumes from a cursor. into_extracted_entities
now tracks a per-surface (byte_after, char_after) cursor in a
HashMap<String, (usize, u32)>, so duplicate mentions get distinct
non-overlapping spans and over-claim (LLM returns 3 "Alice" when
source has 2) drops the excess entries with a debug log.
3. combine() always included w.llm_importance in the denominator even
when llm_importance=0.0 because the LLM didn't run. That artificially
lowered the total on short-circuit paths. score_chunk now branches on
llm_consulted: uses combine() when LLM ran (importance actually
contributes), combine_cheap_only() when LLM was skipped or failed
(denominator excludes the LLM weight). Observable in two new tests:
short_circuit_reports_cheap_only_total verifies r.total ==
combine_cheap_only(r.signals, weights) and strictly exceeds
combine(r.signals, weights) when llm_importance=0; llm_consulted_
reports_full_total verifies the opposite path.
4. Same issue from a different angle (CodeRabbit flagged both). Fixed
by the same llm_consulted branch.
Test-literal fixes:
- signals/ops.rs: ScoreSignals literals pick up llm_importance field;
make_entities helper uses ..Default::default() on ExtractedEntities.
- extract/types.rs: three test ExtractedEntities literals gain
llm_importance: None, llm_importance_reason: None.
- resolver.rs: canonicalise_batch_preserves_spans literal gains the
two fields.
- score/store.rs: sample_row gains signals.llm_importance and
llm_importance_reason.
New tests (7 added):
- find_char_span_from_advances_past_prior_match
- find_char_span_from_returns_none_after_exhaustion
- find_char_span_from_preserves_utf8
- find_char_span_from_rejects_non_char_boundary
- into_extracted_entities_gives_distinct_spans_to_duplicate_mentions
- into_extracted_entities_drops_extra_duplicate_when_source_only_has_one
- extract_soft_fallback_on_unreachable_endpoint (transport failure →
empty extraction, not Err)
- short_circuit_reports_cheap_only_total
- llm_consulted_reports_full_total
cargo check --lib clean; cargo fmt clean. The integration-test rmeta
errors visible locally are stale-metadata artifacts from incremental
builds with prior struct shapes; CI's clean build resolves them and
tests/*.rs files do not construct any of the touched types.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(memory): source trees + bucket-seal foundation (#709)
Phase 3a of the memory architecture (#711 umbrella). Lifts admitted leaves
into a per-source hierarchy so Phase 4 retrieval has a tree to walk.
## What's in this PR
* **Schema** — three new tables in `chunks.db` alongside Phase 1/2:
`mem_tree_trees`, `mem_tree_summaries`, `mem_tree_buffers`. Plus a
`parent_summary_id` backlink column on `mem_tree_chunks`. All migrations
are additive and idempotent (`ALTER TABLE ADD COLUMN`, same pattern as
Phase 2).
* **`source_tree/` module** — isolated subdir; does not touch the legacy
`openhuman::memory` layer.
- `types.rs` — `Tree`, `SummaryNode`, `Buffer`, `TreeKind` (Source/Topic/Global),
`TreeStatus`. Constants: `TOKEN_BUDGET = 10_000`, `DEFAULT_FLUSH_AGE_SECS = 7d`.
- `store.rs` — CRUD for all three tables via the shared `with_connection`
entry point. Writes are transactional; summary insert is idempotent
on primary key (INSERT OR IGNORE).
- `registry.rs` — `get_or_create_source_tree(scope)` — idempotent lookup
keyed on `(kind, scope)`.
- `bucket_seal.rs` — `append_leaf` + cascade: push a leaf into L0, seal
when `token_sum >= TOKEN_BUDGET`, recurse upward. One seal = one
transaction; children get a parent backlink (leaves via
`parent_summary_id`, summaries via `parent_id`). Tree's `max_level`
and `root_id` bump on root split.
- `flush.rs` — `flush_stale_buffers(max_age)` force-seals any buffer
whose `oldest_at` is older than `max_age`. Primitive only; wiring
into a scheduler is out of scope.
- `summariser/` — `Summariser` trait + `InertSummariser` fallback
(concatenate children, union entities preserving first-seen order,
hard-truncate to budget). Trait is async; Ollama implementation slots
in later without breaking callers.
* **Ingest wiring** — `tree/ingest.rs::persist` now calls `append_leaf`
once per kept chunk after chunks + scores land. Failures are logged at
warn level and do not fail the ingest — leaves are already persisted
and the next flush can still rebuild the tree.
## Concurrency / LLM
The async summariser call happens OUTSIDE any DB transaction, so a slow
LLM never holds SQLite locks. Blocking DB calls inside `append_leaf` are
acceptable for Phase 3a because the Inert summariser does no real I/O;
when a networked summariser lands, wrap the DB calls in
`tokio::task::spawn_blocking`.
## Tests (23 new, all green)
* `types` — enum round-trips, buffer staleness predicates
* `store` — tree insert + unique scope, summary insert + idempotence,
buffer upsert/clear, stale-buffer ordering
* `registry` — `get_or_create` idempotence, distinct scopes yield distinct
trees, id prefix format
* `summariser/inert` — provenance-prefixed concat, entity union with
order preservation, budget truncation, empty-content skip
* `bucket_seal` — append-below-budget is buffered only (no seal);
12k-token cross-over produces an L1 summary with correct `child_ids`,
L0 cleared, L1 carries the new summary id, tree `max_level`/`root_id`
updated, leaf → parent backlink populated
* `flush` — stale buffer force-seals under budget; recent buffer is a no-op
Broader `memory::tree` suite: 160 tests pass (23 new + 137 existing Phase
1/2), no regressions.
## Stacked on #775
This PR applies on top of `feat/708-memory-llm-ner` (PR #775, Phase 2
LLM-NER follow-up). Merge #775 first; this branch will rebase cleanly
onto main.
## Out of scope (tracked separately)
* **3b — Global activity digest tree** — time-aligned cross-source recap
* **3c — Topic trees** — hotness-driven per-entity materialisation
* **Phase 4 (#710)** — retrieval / query / gate
Closes part of #709 (source-tree foundation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(memory): cargo fmt on source_tree module (#709)
CI rustfmt caught a few multi-line function signatures and import
groups that rustfmt wants collapsed / reordered. No behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(memory): address CodeRabbit review on source-tree (#709)
Three correctness fixes from CodeRabbit on PR #789, plus a design
clarification for the InertSummariser. False-positive findings
(schema-out-of-sync, rustfmt — already in
|
||
|
|
2b53566283 |
feat(memory): LLM-based NER + importance signal with cheap-signals short-circuit (#708) (#775)
* feat(memory): LLM-based NER + importance signal with cheap-signals short-circuit (#708) Adds an Ollama-backed entity extractor and an LLM-derived importance score as a new signal in the admission gate. Builds on merged Phase 2 (#733) without changing its surface — additive only. Why LLM (not GLiNER): zero new deps (reuses existing reqwest + async infra), reuses the same Ollama setup openhuman uses for embeddings, better per-entity quality scales with model choice, no native ONNX runtime to ship. Latency cost (~100-300ms per chunk on a small model like qwen2.5:0.5b) is amortised by the short-circuit. The admission gate stays a hybrid - cheap deterministic signals always run; the LLM is one signal among many, never the sole arbiter. Backwards-compatible: with default SignalWeights the LLM weight is 0.0 and behaviour matches pre-LLM Phase 2 exactly. What's new: - src/openhuman/memory/tree/score/extract/llm.rs: LlmEntityExtractor implementing the existing EntityExtractor trait. Posts a structured JSON request to an Ollama-compatible /api/chat endpoint asking for NER + an importance rating in one call. Span recovery via string search; hallucinated entities (surface not in source text) dropped. Soft fallback: HTTP failures log a warn and return empty extraction. - ExtractedEntities gains llm_importance (Option<f32>) + llm_importance_reason (Option<String>); merge() takes max importance. - ScoreSignals gains llm_importance (f32, defaults to 0.0). - SignalWeights gains llm_importance (default 0.0; with_llm_enabled() helper sets it to 2.0 - comparable to metadata/source weights). - signals::combine_cheap_only(): variant that excludes the LLM signal, used for the short-circuit decision in score_chunk. - ScoringConfig gains llm_extractor (Option<Arc<dyn EntityExtractor>>), definite_keep_threshold (default 0.85), definite_drop_threshold (default 0.15). New with_llm_extractor() constructor. - score_chunk() pipeline: 1. Always-on regex extraction 2. Cheap signals + combine_cheap_only -> cheap_total 3. If cheap_total >= definite_keep OR <= definite_drop: skip LLM 4. Else (borderline band): run LLM extractor, merge results, recompute 5. Final combine + admission gate against drop_threshold - mem_tree_score table gains nullable llm_importance + llm_importance_reason columns via idempotent ALTER TABLE migration. - ScoreRow + upsert/get persist the new column. What's not changed: - Existing JSON-RPC surface - Default behaviour (LLM weight=0, no llm_extractor configured = same outputs as before) - mem_tree_chunks / mem_tree_entity_index schemas Tests added: - LLM extractor: prompt construction, JSON parsing, hallucination drop, importance clamping, strict vs lenient unknown-kind handling - Score pipeline: short-circuit on definite_keep/definite_drop skips LLM call (verified via FakeLlm call counter); borderline band consults LLM once; LLM failure falls back gracefully without erroring LLM-NER work follows up on #708. Parent: #711. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(memory): address CI failures and CodeRabbit review on LLM-NER (#708) CI was red on PR #775 due to 10 E0063 errors — existing test-only struct literals for ExtractedEntities / ScoreSignals / ScoreRow weren't updated when the new optional llm_importance fields landed. Local cargo check on lib-only passed; cargo check --tests caught them, as does CI. CodeRabbit also flagged four semantic issues: 1. LlmEntityExtractor::extract() returned Err on HTTP transport, non-2xx status, and JSON parse failures. The soft-fallback contract (documented in the module header) only worked because score_chunk catches errors; any other caller got a hard error. Now split into extract_or_empty() which logs a warn on each failure mode and returns ExtractedEntities::default() unconditionally. The public extract() stays on the async-trait surface but never returns Err. 2. find_char_span() always searched from byte offset 0, so when the LLM returned the same surface twice (explicitly asked for duplicates in the prompt), both entities got (start=0, end=len) — same span, not distinct mentions. Added find_char_span_from(haystack, needle, byte_from, char_from) that resumes from a cursor. into_extracted_entities now tracks a per-surface (byte_after, char_after) cursor in a HashMap<String, (usize, u32)>, so duplicate mentions get distinct non-overlapping spans and over-claim (LLM returns 3 "Alice" when source has 2) drops the excess entries with a debug log. 3. combine() always included w.llm_importance in the denominator even when llm_importance=0.0 because the LLM didn't run. That artificially lowered the total on short-circuit paths. score_chunk now branches on llm_consulted: uses combine() when LLM ran (importance actually contributes), combine_cheap_only() when LLM was skipped or failed (denominator excludes the LLM weight). Observable in two new tests: short_circuit_reports_cheap_only_total verifies r.total == combine_cheap_only(r.signals, weights) and strictly exceeds combine(r.signals, weights) when llm_importance=0; llm_consulted_ reports_full_total verifies the opposite path. 4. Same issue from a different angle (CodeRabbit flagged both). Fixed by the same llm_consulted branch. Test-literal fixes: - signals/ops.rs: ScoreSignals literals pick up llm_importance field; make_entities helper uses ..Default::default() on ExtractedEntities. - extract/types.rs: three test ExtractedEntities literals gain llm_importance: None, llm_importance_reason: None. - resolver.rs: canonicalise_batch_preserves_spans literal gains the two fields. - score/store.rs: sample_row gains signals.llm_importance and llm_importance_reason. New tests (7 added): - find_char_span_from_advances_past_prior_match - find_char_span_from_returns_none_after_exhaustion - find_char_span_from_preserves_utf8 - find_char_span_from_rejects_non_char_boundary - into_extracted_entities_gives_distinct_spans_to_duplicate_mentions - into_extracted_entities_drops_extra_duplicate_when_source_only_has_one - extract_soft_fallback_on_unreachable_endpoint (transport failure → empty extraction, not Err) - short_circuit_reports_cheap_only_total - llm_consulted_reports_full_total cargo check --lib clean; cargo fmt clean. The integration-test rmeta errors visible locally are stale-metadata artifacts from incremental builds with prior struct shapes; CI's clean build resolves them and tests/*.rs files do not construct any of the touched types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |