Commit Graph
1237 Commits
Author SHA1 Message Date
3dc0d0db64 feat(notifications): native OS banners for agent/chat/socket events (Phase 1c of #395) (#780)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
2026-04-22 17:50:21 -07:00
Steven EnamakelandGitHub 98b47b107d feat(pr-manager): post deferred items as GitHub PR review comments (#816) 2026-04-22 17:45:51 -07:00
5113b0fd8b fix(rewards): add retry on snapshot load failure + tab-switch logging (#768)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-22 15:21:07 -07:00
c78c957821 feat(notifications): in-app notification center (Phase 1b of #395) (#769)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
2026-04-22 15:17:45 -07:00
Steven EnamakelandGitHub 0ad526887c feat(agents): add pr-reviewer for CodeRabbit-style PR reviews (#815) 2026-04-22 15:17:09 -07:00
44fda19734 fix(recovery): prompt users to download latest build after crash loops (#778)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
2026-04-22 15:07:48 -07:00
Steven EnamakelandGitHub 1e3743f28e feat(agents): add pr-manager-lite for already-checked-out PR branches (#813) 2026-04-22 14:51:17 -07:00
Steven EnamakelandGitHub 488dbbd7ec feat(homebrew): add homebrew-core formula (#810) 2026-04-22 14:51:02 -07:00
Steven EnamakelandGitHub 15a9c6f780 docs: rename Referral-doc.md to docs/referral-doc.md (#811) 2026-04-22 14:50:09 -07:00
83aa0166d6 feat(onboarding,ui): trust-first onboarding + Button primitive + honest privacy surface (#759)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
2026-04-22 14:49:27 -07:00
9a6f9cd110 fix(onboarding): show onboarding immediately after bootstrap (#777)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
2026-04-22 14:48:40 -07:00
7961a328ca ci(e2e): add Linux workflow for agent-review spec (#763)
Dedicated narrow workflow that runs only agent-review.spec.ts under
tauri-driver + Xvfb and uploads app/test/e2e/artifacts/**. Gates on
spec presence so it can land before the spec itself. Intentionally does
not re-enable the broader commented E2E matrix in test.yml.

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-22 14:31:59 -07:00
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>
2026-04-22 14:30:31 -07:00
8212438112 feat(e2e): agent-observable artifact capture for onboarding + privacy flow (#762)
Adds a canonical WDIO spec that captures screenshots, page-source dumps, and
mock request logs at named checkpoints, plus an afterTest hook that dumps
failure artifacts. Wrapper script prints the run dir so coding agents (and
humans) can inspect the flow from disk.

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
2026-04-22 14:28:34 -07:00
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>
2026-04-22 14:27:47 -07:00
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>
2026-04-22 14:13:35 -07:00
340cbc04f2 test(e2e): cover stub webhooks ingress surface (#795)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-22 13:57:34 -07:00
Mega MindandGitHub 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
2026-04-22 13:47:39 -07:00
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>
2026-04-22 13:45:12 -07:00
Steven EnamakelandGitHub e5e962d10e feat(agents): add pr-manager agent for review comment triage (#808)
* feat(agents): add pr-manager agent for review comment triage

Handles a PR end-to-end: checks it out, works through CodeRabbit and
maintainer review comments, runs the quality suite, auto-fixes
formatting, pushes back, and waits for CodeRabbit re-review before
finalizing.

* chore(agents): add Codex PR manager
2026-04-22 13:43:35 -07:00
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>
2026-04-22 13:40:08 -07:00
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>
2026-04-22 13:39:58 -07:00
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 b7395a8) are not addressed
here because they are not real issues.

## Changes

### Idempotent buffer append (`bucket_seal.rs`)
`append_to_buffer` commits before the async seal/cascade runs, so a
retry after a failed seal (summariser timeout, crash, transient ingest
error) would re-push the same leaf and double-count tokens. Added a
dedup check on `item_ids` so the append is a true no-op for
already-buffered chunks.

### InertSummariser: honest stub, no entity propagation (`summariser/inert.rs`)
Summary-level entities and topics are **LLM-derived from the summary's
own synthesised content** by design, not a mechanical union of
children's labels. The networked summariser (future) does its own
extraction on its output. The inert fallback has no NER, so it emits
empty entities/topics — an honest stub rather than a misleading union.
Removed `union_preserve_order` helper and updated the corresponding
test.

### Forward-compat entity index on seal (`bucket_seal.rs`, `score/store.rs`)
Added `index_summary_entity_ids_tx` — a summary-specific indexer that
takes canonical ids only (matching the `Vec<String>` shape of
`SummaryOutput.entities`) and writes them into `mem_tree_entity_index`
with `node_kind='summary'`, placeholder `entity_kind`/`surface` (both
set to the canonical id), and the summary's score. Called from
`seal_one_level`'s write transaction. No-op today (InertSummariser
emits empty), but wired so the Ollama summariser lands as a
drop-in without touching bucket-seal.

### UNIQUE race in `get_or_create_source_tree` (`registry.rs`)
Two concurrent callers for the same scope could both pass the lookup
and then race on `insert_tree`; the loser got a UNIQUE violation
bubbled as an error instead of the pre-existing row. Now catches
`ConstraintViolation` (and the message-based fallback for chained
errors), re-queries by scope, and returns the winning row. New test
`get_or_create_recovers_from_unique_race` covers both the recovery
path and the `is_unique_violation` detector.

## Tests

161 passed / 0 failed across `memory::tree` (+1 new test vs the
prior baseline of 160). No regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(memory): cargo fmt on source_tree::registry (#709)

CI's rustfmt check flagged the race-recovery block in
get_or_create_source_tree — rustfmt prefers chaining
`?.ok_or_else(...)` on one line and wrapping the assert! macro.
No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(memory): index_summary_entity_ids_tx writes parseable entity_kind (#709)

CodeRabbit on PR #789 caught a bug in the summary entity indexer I
added for Phase 3a. The helper was writing the full canonical id
(e.g. "email:alice@example.com") into the `entity_kind` column instead
of the kind prefix. `lookup_entity()` later runs `EntityKind::parse()`
on that column and would fail with `unknown EntityKind` the first time
a result set mixed leaf and summary hits — blocking any Phase 4 read
that needed summary-level entity resolution.

Fix: split the canonical id on `:` and store the prefix only. Falls
back to the full id with a `warn!` for malformed ids that lack `:`,
so bad data surfaces rather than silently stays latent.

Added regression test `summary_entity_index_kind_is_parseable` that:
- Indexes a leaf entity row (the existing happy path)
- Indexes two summary entity rows via index_summary_entity_ids_tx
- Runs lookup_entity for email, hashtag, and a cross-kind id
- Asserts each row comes back with a correctly-parsed EntityKind

Before the fix, the lookup_entity calls in this test would fail with
a FromSqlConversionFailure on the summary row's entity_kind column.
After the fix, mixed leaf+summary lookups round-trip cleanly.

Tests: 162 passed / 0 failed in memory::tree (+1 vs 161 baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(memory): promote extracted topics to canonical entities (#709)

Addresses a design gap surfaced during PR #789 review: the Phase 3
plan promised topic trees would scope to "entities / topics" but the
implementation routed on canonical_entities only. A chunk saying
"Phoenix migration ships Friday" produced `r.extracted.topics =
["phoenix", "migration"]` but `canonical_entities = []` — meaning no
topic tree named Phoenix would ever spawn.

Fix (Shape A from the review discussion): extend the canonical entity
stream with topic rows. No new tables, no new routing path, no new
hotness counters.

## Changes

- `score/extract/types.rs`: add `EntityKind::Topic` with as_str/parse
  round-trip. `non_exhaustive` enum so this is an additive change.
- `score/resolver.rs::canonicalise`: after emitting canonical entities
  from `extracted.entities`, also emit one per `extracted.topics`
  entry with `kind: Topic`, `canonical_id: "topic:<lowercased>"`.
  Span fields are 0 (topics are chunk-level, not substring-scoped).
  Dedups same-label topics internally; keeps `hashtag:launch` and
  `topic:launch` as separate entities by design.

## Downstream effect (zero-touch)

- `score::store::index_entities_tx` indexes topic entities the same
  way as email entities — rows land in `mem_tree_entity_index` with
  `entity_kind = "topic"`.
- Phase 3c routing iterates `canonical_entities` — topics now trigger
  topic-tree routing automatically. A chunk about "phoenix" starts
  accumulating hotness for `topic:phoenix`; once the threshold
  crosses, `get_or_create_topic_tree("topic:phoenix")` materialises
  a dedicated topic tree. Backfill via `lookup_entity("topic:phoenix")`
  then hydrates historic leaves.
- Phase 4 retrieval can filter `WHERE entity_kind = 'topic'` to
  surface themes without mixing in people/emails.

## Tests

Five new tests in `resolver.rs`:
- topics → canonical entities with `kind: Topic`
- lowercase normalisation + dedup on label
- hashtag + topic with the same label coexist (different kind prefix)
- entities emitted first, topics appended
- `EntityKind::Topic` round-trips through `parse` / `as_str`

Broader memory::tree: 167 passed / 0 failed (162 baseline + 5 new).
No regressions.

## Caveats for follow-up

1. Topic noise — LLM-extracted themes are softer signal than regex
   entities. Hotness threshold (TOPIC_CREATION_THRESHOLD=10.0) gates
   tree creation, so transient topics won't spawn trees.
2. Topic/hashtag duplication — "Phoenix #phoenix" creates both
   `topic:phoenix` and `hashtag:phoenix` trees. Tolerable (kinds are
   semantically distinct) but a dedup policy may be worth revisiting
   in a Phase 4 retrieval follow-up.
3. Topic normalisation is lowercase+trim only. Plurals / stemming
   are deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
2026-04-22 13:28:19 -07:00
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>
2026-04-22 13:26:00 -07:00
CodeGhost21andGitHub b3b451fd28 fix(composio): keep chat integrations cache in sync on Windows (#749)
On Windows the `ComposioConnectionCreated` → `wait_for_connection_active`
invalidation path often misses: the 60 s readiness poll can expire before
the user finishes the hosted OAuth flow (Defender SmartScreen, slower
browser launch, extra consent dialogs), so `invalidate_connected_integrations_cache`
never fires and the chat runtime stays frozen on the pre-connect snapshot
while Settings correctly shows "Connected" via its 5 s `listConnections`
poll.

Layer two cross-platform defenses on top of the existing event-bus path:

1. `composio_list_connections` (the RPC the UI polls every 5 s) now
   diffs the backend's active-toolkit set against every cached entry
   and invalidates on divergence, so chat converges to truth within one
   poll interval — regardless of how the OAuth round-trip completed.
2. Add a 60 s TTL on the `INTEGRATIONS_CACHE` so stale entries can't
   live forever even if nothing polls.
3. Grep-friendly `[composio][integrations]` logs at every decision
   point (cache hit / miss / expiry / divergence diff) for quick
   diagnosis from user-supplied debug dumps.

Also covers the regression with six new unit tests (activate, disconnect,
steady state, non-active rows, CONNECTED vs ACTIVE alias, TTL expiry)
serialized via a shared test mutex so they don't race the existing
mock-backend tests over the process-wide cache.
2026-04-22 18:17:39 +05:30
Cyrus GrayandGitHub 8ab8c59961 fix(onboarding): remove referral code step from onboarding flow (#772)
Simplify onboarding from 4 steps to 3: Welcome → Skills → Context Gathering.
The referral step added unnecessary friction; the feature may return later
so ReferralApplyStep.tsx is preserved but unused.

Closes #752
2026-04-22 14:11:54 +05:30
Cyrus GrayandGitHub 100b5b781c fix(voice): friendly STT error with direct settings link (#761)
* fix(voice): show friendly error with settings link when STT model is missing

Replace technical "whisper.cpp binary not found" error with a user-friendly
message and a "Set up" link that navigates directly to Settings > Local AI
Models so non-technical users can easily download the required speech model.

* style: apply prettier formatting
2026-04-22 12:34:42 +05:30
Steven EnamakelandGitHub 758958bfd5 feat(webview): zero-injection CDP migration for 5 providers (#751)
* feat(webview): zero-injection CDP migration for 5 providers

Moves the per-provider webview scraping path off injected JS and onto
CDP for the cef runtime. The 5 migrated providers (whatsapp, telegram,
slack, discord, browserscan) now load with zero initialization_script;
their UA fingerprint, DOM chat-list scraping, and multi-account target
matching all run through the Chrome DevTools Protocol.

Shared infrastructure in new `cdp/` module: CdpConn, Snapshot walker
(generic DOMSnapshot.captureSnapshot), UaSpec + setUserAgentOverride
helper, per-account session opener that keeps a long-lived CDP session
attached so the UA override stays resident for the lifetime of the
webview. Webview opens at a data:text/html placeholder, CDP applies UA
override, then Page.navigate drives it to the real provider URL with a
`#openhuman-account-{id}` fragment used by all scanners for
multi-account disambiguation (replaces Telegram's title-marker JS).

Per-provider dom_snapshot.rs files for telegram/slack/discord replicate
the WhatsApp scraper pattern. Emit the same `webview:event` ingest
envelope the old recipes used so the frontend is unchanged.

Deferred (kept JS-injected, separate PRs):
- gmail/linkedin: no Rust scanner yet
- google-meet: 535-line lifecycle + captions recipe
- idb.rs `Runtime.callFunctionOn` serializer: audit listed as future work

Also removes the unused `webview_account_eval` Tauri command.

Wry builds retain the legacy ua_spoof.js + runtime.js + recipe.js path
where those files still exist (no wry-specific regressions introduced).

* fix(release): enable Ubuntu 22.04 platform support in release workflow

Uncommented and configured the Ubuntu 22.04 platform in the release workflow to support the x86_64-unknown-linux-gnu target. This change enhances cross-platform compatibility for the release process.

* fix(webview): address CodeRabbit review comments

- session.rs: boundary-check the `starts_with(real_url)` match so
  `https://discord.com` can't accidentally match `https://discord.com.evil/…`
  when deciding whether to skip Page.navigate.
- session.rs: drop unused `_app: AppHandle<R>` parameter from spawn_session
  (and its generic R); update the webview_accounts call site accordingly.
- session.rs: document the non-graceful shutdown path (cancellation token
  left as a follow-up).
- discord/slack/telegram scanners: import CDP_HOST/CDP_PORT from the shared
  `cdp` module instead of redeclaring them. (whatsapp_scanner keeps its
  locals — the module isn't cef-gated so it can't pull from cef-only cdp::.)
- webview_accounts/mod.rs: collapse the duplicate provider_is_supported
  check with provider_url into one early-return.
- emulation.rs: add a TODO documenting when/how to refresh the hardcoded
  Chrome UA fingerprint fields.
- discord/slack/telegram dom_snapshot: hash every row for change detection
  (was capped at first 5–8 — a reorder past that limit wouldn't invalidate
  the cache).
- discord_scanner/dom_snapshot: simplify is_channel_row (single
  attr lookup); tighten the pure-unread-marker comment (dropped the
  obsolete recipe.js cross-reference).
- slack_scanner/dom_snapshot: align find_badge with the Discord behavior
  (empty badge text → Some(0), matching the marker-present-but-no-count
  semantics).

Deferred (explicitly called out in the PR): shared CdpConn dedup across
scanners, shared DomScan/ChannelRow types across provider dom_snapshot
files, graceful session shutdown, table-driven scanner startup macro.

* fix(webview): CR round-2 — session teardown, exact target match, real-url validation

- cdp/session.rs: use exact equality for the per-account target match
  (`t.title == marker || t.url.ends_with(&fragment)`), so
  `…account-abc` can't be mistaken for `…account-abcdef`. Scanners
  (whatsapp/telegram/slack/discord) also switched from `url.contains` to
  `url.ends_with` on the fragment for the same reason.
- cdp/session.rs: `spawn_session` now returns `JoinHandle<()>`.
- webview_accounts: store the CDP session handle keyed by account_id in
  `WebviewAccountsState.cdp_sessions`, abort any prior handle before
  spawning a new one on re-open, and abort on close/purge so reopen
  cycles don't stack live CDP loops.
- webview_accounts: validate `real_url_str` as a `Url` up front (was
  only validating the placeholder), so a malformed `args.url` fails the
  command instead of crashing the async session loop later.
- webview_accounts: `provider_is_supported` now derives from
  `provider_url` (single canonical registry, no drift).
- telegram_scanner/dom_snapshot: row ids now always include `idx`, so
  two chats with the same display name don't collide into one id.

* fix(webview): CR round-3 — scanner prefix, empty-rows emit, wry UA fallback

- DOM poll: drop `!scan.rows.is_empty()` guard in discord/slack/telegram
  fast-ticks so the zero-unread transition (last chat read) still emits a
  hash-change snapshot. Consumers lose the "clearing" state otherwise.
- webview_accounts: derive `scanner_url_prefix` from the validated
  `real_url`'s origin (via `Url::origin().ascii_serialization()`), not
  the static `provider_url(...)`. Debug `args.url` overrides and
  alternate hosts now drive the scanner target match correctly.
- webview_accounts: cfg-split `build_init_script`. Under cef, migrated
  providers return an empty script (zero injection, unchanged). Under
  wry, migrated providers that fingerprint on `navigator.*` still ship
  `ua_spoof.js` even though their recipe is gone — the previous early
  return dropped the UA shim, which would regress Slack/Google login
  gates on wry dev builds.
2026-04-21 23:15:31 -07:00
6a28e75e94 feat(routing): pluggable local inference (MLX, llama.cpp, LM Studio) + openhuman:// deep-link + OpenSSL 3 PKCS12 fix (#750)
* feat(routing,local-ai): pluggable local inference + openhuman:// deep-link

Salvaged from recovery commit cfb1fd9f (Apr 19) — originally auto-backed up
by claude-mem recovery tool, not yet PR'd.

## Local AI routing becomes pluggable (factory.rs, health.rs)

Previously the IntelligentRoutingProvider hard-wired Ollama as the only local
inference backend. This commit lets operators point at any OpenAI-compatible
local server (llama.cpp / llama-server, LM Studio, MLX's mlx_lm.server /
mlx-omni-server, vLLM — anything exposing /v1/chat/completions + /v1/models).

Config surface:
- `OPENHUMAN_LOCAL_INFERENCE_URL` env var — full /v1 base URL. When set,
  health is probed via GET {base}/models (OpenAI-compat) instead of Ollama's
  /api/tags.
- `LocalAiConfig.provider` accepts "llamacpp" or "llama-server" as aliases
  that default to http://127.0.0.1:8080/v1.
- Default (no env, no override) stays Ollama — zero config change for
  existing users.

Motivation: Ollama's embedded llama.cpp cannot yet load Gemma 4 E2B and
other recent models; MLX is significantly faster on Apple Silicon.
Operators should be able to pick the backend; end users see nothing.

## Deep-link scheme (Info.plist)

Adds CFBundleURLTypes entry registering `openhuman://` for the macOS bundle
so browser-issued deep links (e.g. openhuman://auth?token=...) launch the
installed app. See .claude/rules/14-deep-link-platform-guide.md for the
flow — requires a .app bundle (not `tauri dev`).

## OpenSSL 3.x PKCS12 fix (setup-dev-codesign.sh)

Adds `-legacy` to the `openssl pkcs12 -export` call. Required on OpenSSL 3.x
because the modern SHA-256 MAC / AES-256-CBC defaults are not yet supported
by macOS `security` tool, which silently fails to import the cert.

Notes:
- Dropped the CLAUDE.md.new noise and the mic-description copy downgrade
  from the original recovery commit; kept only the load-bearing changes.
- Cargo.lock files reset to upstream/main to avoid dependency drift — the
  new code does not introduce new deps.

Co-Authored-By: WOZCODE <contact@withwoz.com>

* fix(routing,codesign): address CodeRabbit review on PR #750

- setup-dev-codesign.sh: probe for openssl `-legacy` support before
  using it so older OpenSSL/LibreSSL installs don't silently fail;
  drop the stderr suppression on pkcs12 so import errors surface.
- routing/factory.rs: trim provider before alias matching, lower the
  local-inference diagnostic to `debug` and drop raw URLs from the
  log fields, and honor `OPENHUMAN_OLLAMA_BASE_URL` via the existing
  `ollama_base_url()` helper in the default Ollama branch.
- local_ai/mod.rs: re-export `ollama_base_url` for the routing factory.

* style(app/src-tauri): apply rustfmt to merged main

`cargo fmt --check` in the merge workspace flagged two hunks inherited
from upstream main:

- app/src-tauri/src/lib.rs: move `mod notification_settings;` between
  the cfg(cef) imessage_scanner and slack_scanner declarations so the
  ordering matches rustfmt's reordering output.
- app/src-tauri/src/webview_accounts/mod.rs: wrap the long
  `try_state::<NotificationSettingsState>()` binding.

No behavior change.

* format

---------

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
2026-04-21 23:15:09 -07:00
Steven EnamakelandGitHub e405e2cc99 Prune obsolete config API keys (#739)
* feat(memory_recipes): introduce memory recipes module for source-type-specific ingestion

- Added a new `memory_recipes` module that includes functionality for handling various source types, such as email and chat, with dedicated recipes for ingestion.
- Implemented core types and the `Recipe` trait to facilitate the ingestion process, allowing for structured handling of raw content and metadata.
- Created specific recipes for email and chat, enhancing the ability to process and normalize content before storing it in memory.
- Updated the core controller registration to include new endpoints for memory recipes, enabling JSON-RPC interactions for ingesting content and listing available recipes.
- Enhanced task management by integrating task storage and query helpers, allowing for better tracking and management of action items extracted during ingestion.

These changes significantly improve the ingestion capabilities of the application, providing a robust framework for handling diverse content types.

* refactor: remove API key references from configuration and schema

- Eliminated the `api_key` field from various configuration schemas and related structures, including `Config`, `ModelSettingsPatch`, and `ComposioConfig`, to streamline the configuration process.
- Updated the handling of API keys in the application to rely solely on session JWTs, enhancing security and simplifying the authentication flow.
- Adjusted related documentation and comments to reflect the removal of the API key, ensuring clarity in the new authentication approach.
- Refactored multiple components and functions to remove dependencies on the API key, improving code maintainability and reducing complexity.

These changes significantly enhance the security posture of the application by removing reliance on static API keys and promoting the use of session-based authentication.

* chore: update .env.example and documentation for API key removal

- Removed references to `OPENHUMAN_API_KEY` from the `.env.example` file to reflect recent changes in the authentication approach.
- Updated documentation in `AGENTS.md` and `CLAUDE.md` to clarify that `OPENHUMAN_API_URL` now overrides `config.api_url`, ensuring consistency across configuration references.
- Bumped the version in `Cargo.lock` to 0.52.23 to align with the latest changes.

These updates enhance clarity in configuration management and documentation following the removal of static API keys.

* fix: address config pruning review comments

* feat(tests): add validation tests for SearchResponse deserialization

- Introduced new tests to ensure that the SearchResponse struct correctly rejects JSON inputs missing required fields: searchId, results, and costUsd.
- Updated DelegateTool instantiation to remove the unused fallback_credential parameter, simplifying the constructor and related tests.

These changes enhance the robustness of the SearchResponse handling and improve code clarity by removing unnecessary parameters.

* refactor: remove api_key params from provider factory signatures

The OpenHuman backend now only uses the app-session JWT for auth —
the api_key parameter was ignored (as _api_key) after the config
pruning. Drop it from the signatures entirely instead of leaving
dead None arguments at every call site.

Also removes the unused ChannelContext.api_key field.

* refactor: remove api_key/composio_key params from factory signatures

Every caller was passing None after the config pruning:

- create_embedding_provider / create_memory* — the api_key param
  was only threaded to the openai / custom embedding branches, but
  no caller has ever supplied a non-None value since the pruning.
  Embeddings use an empty key; the param is gone.

- all_tools / all_tools_with_runtime — both composio_key and
  composio_entity_id only fed the ComposioTool registration block,
  which was already dead after the JWT migration. Drop both args
  and the block; the modern path is all_composio_agent_tools via
  build_composio_client(config).

* style: apply cargo fmt
2026-04-21 22:49:11 -07:00
CodeGhost21andGitHub bdbb83772e feat(observability): Sentry release tracking, source maps, and end-to-end DSN plumbing (#734)
* ci(release): bake Sentry DSN into shipped tauri bundle

Released builds weren't reporting anything to Sentry. Root cause: the
tauri.conf.json `beforeBuildCommand` re-runs `vite build` inside
`cargo tauri build`. The prior `yarn build` step set `VITE_SENTRY_DSN`
for its own run, but the tauri step did not — so the rebuild produced
a DSN-less `dist/` that overwrote the good one, and the shipped web UI
initialized Sentry with an empty DSN (`initSentry` returns early when
`!SENTRY_DSN`).

Fix:

- `release.yml` / `build-desktop` — declare `VITE_SENTRY_DSN` and
  `VITE_DEBUG` on the tauri-build step so the `beforeBuildCommand`
  rebuild bakes them into the final bundle.
- `release-packages.yml` / `build-cli-linux-arm64` — guard against a
  missing `vars.OPENHUMAN_SENTRY_DSN` so the Linux arm64 CLI tarball
  cannot ship without error reporting baked in via `option_env!`.

The core sidecar's `option_env!("OPENHUMAN_SENTRY_DSN")` already gets
the value from the dedicated "Build sidecar core binary" step; the
tauri shell doesn't rebuild it (separate crate, not a workspace dep),
so the baked DSN survives into the bundled installer.

* feat(observability): Sentry release tracking + source maps (#405)

Tags every Sentry event with a canonical release identifier shared by
the frontend and Rust core, uploads source maps so stack traces are
symbolicated in the dashboard, and adds a CLI probe for repeatable
verification of any future release.

Release identifier

  openhuman@<semver>[+<short_git_sha>]

- Frontend (`app/src/utils/config.ts::SENTRY_RELEASE`) builds the tag
  from `VITE_BUILD_SHA`.
- Core sidecar (`src/main.rs::build_release_tag`) builds the same tag
  from `option_env!("OPENHUMAN_BUILD_SHA")`, so events from both
  surfaces group under one release. Cargo's fingerprint already tracks
  `option_env!` changes.

Environment separation

- Frontend: new `APP_ENVIRONMENT` export (`development` | `staging` |
  `production`) derived from `VITE_OPENHUMAN_APP_ENV`, passed to
  `Sentry.init`.
- Core: `resolve_environment` honors `OPENHUMAN_APP_ENV` at runtime,
  falling back to `debug_assertions` detection.

Source-map upload

- `@sentry/vite-plugin` added as an app devDependency.
- `vite.config.ts` emits source maps unconditionally and registers the
  plugin only when `SENTRY_AUTH_TOKEN` is present, so local dev skips
  silently. The plugin uploads `dist/**/*.js{,.map}` under the
  canonical release name and then deletes the on-disk `.map` files so
  they never ship to end users.

CI wiring (`release.yml` + `release-packages.yml`)

- `Build frontend` and `Build and package Tauri app` both receive
  `VITE_BUILD_SHA`, `SENTRY_RELEASE`, `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`,
  `SENTRY_PROJECT_FRONTEND`. The tauri step needs the same env because
  its `beforeBuildCommand` re-runs `vite build`.
- `Build sidecar core binary` receives `OPENHUMAN_BUILD_SHA` so
  `option_env!` bakes the short SHA into the release tag.
- `build-cli-linux-arm64` mirrors `OPENHUMAN_BUILD_SHA` and
  `OPENHUMAN_APP_ENV` for the arm64 CLI tarball.

Verification support

- New `openhuman sentry-test` CLI subcommand captures an `Error`-level
  event against the currently-initialized client, flushes, and prints
  the event UUID. Optional `--panic` flag exercises the panic
  integration. Requires a DSN resolvable at runtime or baked in at
  compile time; exits non-zero otherwise so misconfiguration is loud.
- `src/main.rs` now loads `.env` before `sentry::init`, so a DSN
  defined only in the repo-local dotenv file (common dev case) is
  honored by the startup-time Sentry client.

Docs

- `docs/sentry.md` covers the release identifier, environment table,
  source-map pipeline, required CI variables, and a verification
  runbook with troubleshooting tips.
- `.env.example` + `app/.env.example` document the new build-time vars.
2026-04-21 22:48:20 -07:00
b45e0ed262 refactor(chat): extract bubble components and helpers from Conversations.tsx (#755)
Split the 1,692-line Conversations.tsx by lifting presentational bubbles and
pure helpers into app/src/pages/conversations/. Conversations.tsx now 1,394
lines; semantically identical (verified by Codex + Gemini review).

- components/AgentMessageBubble.tsx (+BubbleMarkdown)
- components/ToolTimelineBlock.tsx
- components/LimitPill.tsx
- utils/format.ts (AgentBubblePosition, formatRelativeTime, formatResetTime,
  getInlineCompletionSuffix, buildAcceptedInlineCompletion)

Typecheck clean, 464 tests pass.

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
2026-04-21 22:41:51 -07:00
8858b3cc59 fix(autocomplete): guard Ctrl+Tab and drop low-quality suggestions (#753)
Three fixes for the system-wide autocomplete accept-on-Tab behaviour:

1. Any-modifier guard: try_accept_via_tab now bails when Shift/Ctrl/Option/Cmd
   is held, so Ctrl+Tab (app-switch), Shift+Tab (outdent), Cmd+Tab and
   Option+Tab no longer trigger accept. Implemented via CGEventSourceFlagsState
   probed through the existing Input Monitoring cache.
2. Quality gate: is_low_quality_suggestion rejects <2-char, whitespace/punct-only
   and tail-echo suggestions before they reach the overlay.
3. Clean edge-state reset when modifier is held so a clean Tab press afterwards
   still accepts.

Tests: 9 unit tests pass, cargo check clean.

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
2026-04-21 22:40:24 -07:00
515c4f4b42 feat(imessage): testable tick harness + fix scanner never ingested (#746)
* fix(agent): orchestrator never sends users to external dashboards for connect

Agent was improvising guidance like 'open your Composio dashboard at
app.composio.dev' when users asked to connect a service, which is broken
UX for non-technical users. Add an explicit rule: route connect requests
to the in-app Settings → Connections path, never paste external URLs
or explain OAuth/Composio internals.

* plan: imessage live-tick harness

Plan doc for extracting run_single_tick from ScannerRegistry's
AppHandle-coupled loop, enabling unit tests with fake deps against
real chat.db and an ignored live-sidecar integration test.

Scaffolding only — code changes follow in subsequent commits.

Co-Authored-By: WOZCODE <contact@withwoz.com>

* feat(imessage): testable tick harness + fix scanner never ingested

Extract pure `run_single_tick` + `TickDeps` trait from `run_scanner`. Prod
path wraps in `HttpDeps`; tests use `FakeDeps` with real chat.db so the
scanner body runs without a Tauri AppHandle. Template for five more
Apple-native sources per the roadmap.

While running it end-to-end, two pre-existing bugs in the shipped scanner
surfaced — both had to be fixed for a tick to ever succeed:

1. `ScannerRegistry::ensure_scanner` called `tokio::spawn` from the Tauri
   `setup` hook, which runs before a Tokio reactor is active. Scanner
   task never spawned; main thread panicked with "no reactor running".
   Fix: `tauri::async_runtime::spawn`, which uses Tauri's own runtime.

2. `fetch_imessage_gate` used JSON pointer `/result/config/...` but the
   JSON-RPC `RpcOutcome` envelope nests one level deeper:
   `/result/result/config/...`. Gate always resolved to None, so every
   tick silently skipped even with iMessage connected. Fix: correct
   the pointer.

Verification: after both fixes, iMessage connected via
`openhuman.channels_connect` with `allowed_contacts=["*"]`, scanner
ingested 252 chat-day groups into `memory_docs` namespace
`imessage_default` — first successful ingest in the project's history.

Tests: 10 unit (9 existing + `skips_when_gate_disconnected`), 4 ignored
live-chat.db (2 existing + `run_single_tick_ingests_groups_from_real_chatdb`
+ `run_single_tick_keeps_cursor_on_group_failure`). All green.

Infra: `scripts/worktree-bootstrap.sh` — one-shot to init submodules,
symlink `.env`, build+stage sidecar, ensure tauri-cli. Worktrees didn't
inherit any of this, costing ~30 min of manual setup per branch.

Docs: `docs/superpowers/learnings/2026-04-21-imessage-harness-session.md`
captures what broke, why, and the debuggability ladder that would have
caught both bugs at merge time (launch-smoke CI, minimum).

Co-Authored-By: WOZCODE <contact@withwoz.com>

* chore(bootstrap): yarn install so husky hooks don't block push

Pre-push hook needs prettier; worktree had no node_modules. Friction
discovered pushing this branch.

Co-Authored-By: WOZCODE <contact@withwoz.com>

---------

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
2026-04-21 22:25:44 -07:00
68214579d6 feat(onboarding): expand SkillsStep to Gmail+Calendar+Drive+Notion with fallback (#743)
Broaden onboarding integration step from Gmail-only to four high-value toolkits
(Gmail, Google Calendar, Google Drive, Notion). Canonicalize backend allowlist
slugs and fall back to the curated static catalog when the allowlist is empty
or hasn't loaded, so users always see actionable connect targets.

- SkillsStep.tsx: 4-toolkit onboarding set, canonicalized slug filter, static
  fallback when backend allowlist returns nothing
- SkillsStep.test.tsx: vitest coverage for fallback, allowlist subset, and
  connected-source forwarding on continue

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-21 22:25:29 -07:00
4a5a00cb96 feat(notifications): webview notifications end-to-end (Phase 7) (#741)
* wip(notifications): core module skeleton + controller registration

Phase 7 PR #714 work-in-progress. Shipped:
- src/openhuman/webview_notifications/ — mod, types, dispatch, bus, schemas
- pub mod declaration in src/openhuman/mod.rs
- controllers entry in src/core/all.rs

Remaining: schemas line in all.rs, Tauri shell edits (OpenHuman: prefix,
feature flag gate, click event emit), new shell module for feature flag
state + commands, frontend app/src/lib/webviewNotifications/ IPC wrapper,
Redux click handler.

* feat(notifications): register webview_notifications schemas in core registry

Adds the schemas line next to the already-registered controllers entry so
the domain participates in declared-schema validation. v1 controller list
is empty (settings live shell-side), but the wiring is in place for
future additions.

* feat(notifications): add OpenHuman: prefix, feature flag, and click-routing event

Updates the CEF notification hook in webview_accounts to:
- Prefix OS toast titles with "OpenHuman:" so they visually disambiguate
  from natively installed apps (Slack, Gmail, Discord desktop) firing the
  same DM twice.
- Gate delivery on a new shell-side feature flag (off by default).
- Mirror each fire to the React frontend via a `webview-notification:fired`
  Tauri event carrying {account_id, provider, title, body, tag}, so the
  UI can route click-to-focus back to the originating webview.

Adds a new notification_settings shell module holding the runtime toggle
as an AtomicBool (lock-free read from the CEF callback thread) plus
notification_settings_{get,set} Tauri commands for the settings UI to
flip the flag. State lives shell-side rather than in the core sidecar so
the toggle doesn't require a JSON-RPC round-trip.

* feat(notifications): frontend IPC wrapper + Redux click routing

Mirrors the Rust-side `webview-notification:fired` Tauri event into
Redux so the UI can:

- bump an unread badge on the originating account (sidebar surface
  reads `accounts.unread[accountId]`)
- route a subsequent click intent back to the right embedded webview
  via `focusAccountFromNotification`, which sets `activeAccountId`
  and clears the unread counter in one action

Plumbing:
- `app/src/lib/webviewNotifications/` — typed subscribe/unsubscribe,
  idempotent `started` guard mirroring `webviewAccountService.ts`,
  `handleNotificationClick(accountId)` as the public click entrypoint
  so in-app toast UI or a future OS-notification click hook share one
  Redux intent
- `accountsSlice`: `noteWebviewNotificationFired` and
  `focusAccountFromNotification` reducers (both no-op for unknown
  account ids — guards against stale payloads from a closed webview)
- `App.tsx`: start the service at boot, right next to the existing
  `startWebviewAccountService()` call

Tests:
- `accountsSlice.webviewNotifications.test.ts` — reducer behavior
- `service.test.ts` — fired dispatches + click focuses via real store

Also: `cargo fmt` noise fixups on `src/core/all.rs` and
`src/openhuman/webview_notifications/mod.rs` so the branch passes
`cargo fmt --check` in CI.

---------

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-21 22:25:12 -07:00
1b6a16f1ab build(sidecar): enable whatsapp-web,channel-matrix features (#742)
Staged openhuman-core sidecar was compiled without these optional
features, so the shipped binary embedded the feature-gated stubs and
failed at runtime with "requires 'whatsapp-web' feature". Wire both
features into the cargo build invocation so the real WA-web and Matrix
listeners make it into the binary.

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-21 22:06:52 -07:00
github-actions[bot] ef657fbf00 chore(release): v0.52.28 v0.52.28 2026-04-22 04:42:12 +00:00
72860b8c89 fix(agent): orchestrator never sends users to external dashboards for connect (#744)
Agent was improvising guidance like 'open your Composio dashboard at
app.composio.dev' when users asked to connect a service, which is broken
UX for non-technical users. Add an explicit rule: route connect requests
to the in-app Settings → Connections path, never paste external URLs
or explain OAuth/Composio internals.

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-21 21:37:33 -07:00
Steven Enamakel 27f3bcd1d2 fix(docs): update Discord link and add highlights section to README.md
- Changed the Discord link to point to the new URL: https://discord.tinyhumans.ai/.
- Added a new "Highlights" section detailing key features of OpenHuman, including Neocortex, The Subconscious, Screen Intelligence, and more, with links to their respective documentation.

These updates improve the clarity and accessibility of the README, ensuring users have the latest information and can easily navigate to important features.
2026-04-21 17:12:24 -07:00
5fff5568d3 feat(memory): Phase 2 memory tree - preprocessing, scoring, admission gate (#708) (#733)
* feat(memory): phase 1 memory tree - multi-source ingestion & canonical chunks (#707)

Adds an isolated memory tree layer under src/openhuman/memory/tree/
implementing Phase 1 of the new memory architecture (umbrella #711).
Zero edits to existing memory/*.rs files - the new layer coexists
with the legacy TinyHumans-backed client.

- Source adapters: chat / email / document -> canonical Markdown
- Token-bounded chunker with deterministic SHA-256 chunk IDs
- SQLite persistence at <workspace>/memory_tree/chunks.db with full
  provenance metadata (source_kind, source_id, owner, timestamps,
  tags, time_range) and back-pointer to raw source
- Unified JSON-RPC ingest (dispatches on source_kind + JSON payload):
  openhuman.memory_tree_ingest, _list_chunks, _get_chunk
- DataSource enum covering the 8 providers from m.excalidraw step 1
  (Discord/Telegram/Whatsapp/Gmail/OtherEmail/Notion/MeetingNotes/DriveDocs)
- ~40 unit tests (chunk ID stability, UTF-8-safe splitting,
  canonicalisation idempotence, store round-trip, filter behavior)

Additive only: new tables in a new DB file, new JSON-RPC namespace,
no existing behavior changes. Feeds #708 (scoring), #709 (summary
trees), #710 (query tools).

Closes #707. Parent: #711.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(memory): phase 2 memory tree - preprocessing, scoring, admission gate (#708)

Adds the scoring / admission layer between Phase 1's chunker and store.
Stacked on feat/707-memory-ingestion (PR #732) - depends on Phase 1's
chunk substrate.

- Pluggable EntityExtractor trait + CompositeExtractor chain
- RegexEntityExtractor: mechanical entities (emails, URLs, @handles, #hashtags)
  - Always on, deterministic, zero deps, UTF-8-safe char spans
- Five weighted signals: token count, unique-word ratio, metadata weight,
  source weight (per-DataSource), interaction (reply/sent/mention/dm tags),
  entity density
- Exact-match entity canonicalisation (email lowercased, @ and # stripped)
- Admission gate drops chunks below configurable threshold (default 0.3)
  - Score rationale persists for EVERY chunk (kept or dropped) for debugging
  - Entities indexed for KEPT chunks only
- Two new SQLite tables added to the memory_tree DB:
  - mem_tree_score: per-chunk score rationale with all signal values
  - mem_tree_entity_index: inverted index entity_id -> node_id
- Idempotent ALTER TABLE migration adds embedding BLOB column to
  mem_tree_chunks (used in Phase 3 retrieval, wired but not populated here)
- Ingest pipeline converted to async to accommodate the extractor trait;
  blocking SQLite work isolated on spawn_blocking; JSON-RPC surface
  unchanged (same memory_tree_ingest / list / get methods)
- Phase 2 deliberately ships without GLiNER/semantic NER - per-chunk
  semantic entities land later behind a cargo feature flag; the composite
  extractor interface keeps that drop-in trivial

Additive only: new tables, new columns, new module. Existing Phase 1
behavior unchanged except that low-signal chunks are now dropped before
reaching mem_tree_chunks. Raise score_drop_threshold to 0 to disable
the gate and restore Phase-1-identical behavior.

Closes #708. Parent: #711. Depends on: #707 (#732).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix memory tree scoring persistence issues

* Fix memory tree scoring robustness issues from PR review

- ingest: fail fast if scorer returns fewer/more results than chunks
  (silent zip truncation would drop chunks or their score rationale)
- score::persist_score{,_tx}: clear stale entity-index rows before
  re-indexing a re-scored chunk, since INSERT OR REPLACE never deletes
  rows whose entity_id is no longer in the new extraction
- score::store::lookup_entity: clamp limit to i64::MAX before casting
  to prevent a large usize wrapping into a negative LIMIT

Adds clear_entity_index_drops_stale_rows regression test.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
2026-04-21 16:17:27 -07:00
0ce1253fe1 feat(memory): Phase 1 memory tree - multi-source ingestion & canonical chunks (#707) (#732)
* feat(memory): phase 1 memory tree - multi-source ingestion & canonical chunks (#707)

Adds an isolated memory tree layer under src/openhuman/memory/tree/
implementing Phase 1 of the new memory architecture (umbrella #711).
Zero edits to existing memory/*.rs files - the new layer coexists
with the legacy TinyHumans-backed client.

- Source adapters: chat / email / document -> canonical Markdown
- Token-bounded chunker with deterministic SHA-256 chunk IDs
- SQLite persistence at <workspace>/memory_tree/chunks.db with full
  provenance metadata (source_kind, source_id, owner, timestamps,
  tags, time_range) and back-pointer to raw source
- Unified JSON-RPC ingest (dispatches on source_kind + JSON payload):
  openhuman.memory_tree_ingest, _list_chunks, _get_chunk
- DataSource enum covering the 8 providers from m.excalidraw step 1
  (Discord/Telegram/Whatsapp/Gmail/OtherEmail/Notion/MeetingNotes/DriveDocs)
- ~40 unit tests (chunk ID stability, UTF-8-safe splitting,
  canonicalisation idempotence, store round-trip, filter behavior)

Additive only: new tables in a new DB file, new JSON-RPC namespace,
no existing behavior changes. Feeds #708 (scoring), #709 (summary
trees), #710 (query tools).

Closes #707. Parent: #711.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(memory): enhance memory tree functionality and tests

- Added support for "memory_tree" namespace description in `namespace_description`.
- Implemented clamping for zero token budget in `chunk_markdown` to prevent empty leading chunks.
- Introduced detailed logging for document ingestion, including title length.
- Updated output schemas in `schemas.rs` for improved clarity.
- Enhanced chunk listing with clamping limits and ordering by sequence in `list_chunks`.
- Normalized source references in canonicalization functions to drop blank values.
- Added comprehensive tests for new features and edge cases in chunking and canonicalization.

These changes improve the robustness and usability of the memory tree layer, aligning with ongoing development efforts for Phase 1 of the memory architecture.

* fix(memory): address follow-up CodeRabbit comments

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
2026-04-21 14:40:27 -07:00
be8cd7b6eb feat(chat): show active token consumption and plan status in UI (#703) (#738)
Adds a small pill in the Conversations chat header with:
- Session token counter — cumulative input + output tokens across all
  chat turns, accumulated from chat:done events via new
  recordChatTurnUsage slice action.
- Plan usage badge — % of 5-hour or weekly budget consumed, driven by
  existing useUsageState. Color-coded sage/amber/coral by severity;
  click navigates to /settings/billing.

No new backend endpoints — session counter is client-local, plan state
reuses the existing creditsApi/billingApi fetches cached by
useUsageState.

Closes #703.

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-21 14:40:08 -07:00
5e0bb97b3b feat(ui): hide TTS and autocomplete entry points (#717) (#737)
Removes user-facing entry points for Voice Dictation/TTS and Autocomplete
per product deprioritization. Routes, panels, modals, and status hooks
are retained so features can be re-enabled by reverting these hunks.

Hidden surfaces:
- Features settings menu: Autocomplete + Voice Dictation cards
- Developer Options: Autocomplete Debug + Voice Debug cards
- Skills page: Text Auto-Complete + Voice Intelligence built-in skill cards
- Chat input: mic/voice mode toggle button

Closes #717, #706.

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-21 14:01:07 -07:00
0dfcb7d71d feat(onboarding): add beta notice banner (#729) (#736)
Shows a dismissible amber banner at the top of the onboarding flow
on first launch. Dismissal is persisted to localStorage so it only
shows once per installation.

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-21 14:00:32 -07:00
8f927369d1 feat(telegram): CDP-driven Telegram Web K scanner (#630) (#638)
* feat(accounts): implement accounts management with webview integration

- Added a new Accounts page for managing user accounts, including the ability to add and remove accounts.
- Introduced AddAccountModal for selecting account providers and initiating account setup.
- Implemented WebviewHost to display third-party web applications (e.g., WhatsApp Web) within the app.
- Enhanced routing to include a protected route for the Accounts page.
- Updated the BottomTabBar to include an Accounts tab for easy navigation.
- Integrated Redux for state management of accounts, messages, and logs, ensuring a seamless user experience.
- Updated dependencies in Cargo.lock to version 0.52.9 for compatibility.

This commit significantly enhances the application's functionality by allowing users to manage accounts directly within the app, improving overall user engagement and experience.

* refactor(accounts): clean up Accounts component layout and improve readability

- Removed unnecessary comments and simplified the structure of the Accounts component for better clarity.
- Adjusted the rendering logic to enhance the layout of the active account section, improving user experience.
- Reformatted text in the no accounts message for better readability.
- Streamlined the import statements by consolidating related imports, enhancing code organization.

* feat(accounts): enhance account management with new providers and routing updates

- Introduced support for additional account providers: Telegram, LinkedIn, Gmail, and Slack, expanding user options for account management.
- Updated routing to replace the old /conversations path with /chat, streamlining navigation and improving user experience.
- Refactored the App component to include an AppShell for better layout management, ensuring the bottom tab bar visibility aligns with the selected account.
- Enhanced the BottomTabBar component to reflect the new routing and account options, improving accessibility and usability.
- Implemented fullscreen logic for accounts, allowing for a more immersive experience when interacting with selected accounts.
- Added utility functions for managing fullscreen states and account provider icons, enhancing code organization and maintainability.

This commit significantly improves the application's account management capabilities, providing users with a more flexible and engaging experience.

* feat(cef): integrate Chromium Embedded Framework support and enhance webview functionality

- Added support for the Chromium Embedded Framework (CEF) as an alternative runtime, allowing for improved webview capabilities.
- Updated Cargo.toml to include new dependencies and features for CEF integration, ensuring compatibility with existing Tauri plugins.
- Enhanced the WhatsApp recipe to include ghost-text autocomplete functionality, improving user experience during message composition.
- Implemented WebSocket observation in the WhatsApp recipe to capture and forward relevant message frames, enhancing real-time interaction.
- Introduced user agent spoofing for specific providers to bypass fingerprinting checks, ensuring better compatibility with services like Slack and LinkedIn.
- Refactored various components to accommodate the new runtime and improve overall code organization and maintainability.

This commit significantly enhances the application's webview capabilities and user interaction with messaging services, providing a more robust and flexible experience.

* feat(cef): add development command for CEF and update configuration

- Introduced a new development command `dev:cef` in both package.json files to streamline the development process for the Chromium Embedded Framework (CEF).
- Updated Cargo.toml to include the `tauri/devtools` feature alongside `tauri/cef`, enhancing debugging capabilities.
- Modified tauri.conf.json to adjust visibility settings for the application window and refined the Content Security Policy (CSP) for improved security.
- Enhanced resource paths in tauri.conf.json to support recursive file inclusion for better resource management.
- Updated the Rust code to bypass macOS Keychain prompts when using CEF, improving user experience during development.

This commit enhances the development workflow for CEF integration, providing better tools and configurations for developers.

* fix(cef): update development command for CEF to include signing identity

- Modified the `dev:cef` command in package.json to include the `APPLE_SIGNING_IDENTITY` environment variable, enhancing the development process for CEF on macOS.
- This change improves the build process by ensuring proper code signing during development, streamlining the workflow for developers working with CEF integration.

* feat(cef): enhance development command for CEF with safe storage setup

- Updated the `dev:cef` command in package.json to include a call to a new script, `setup-chromium-safe-storage.sh`, which pre-seeds the "Chromium Safe Storage" keychain entry with a permissive ACL.
- Added the `setup-chromium-safe-storage.sh` script to ensure that CEF/Chromium can read the keychain entry without prompting, improving the development experience on macOS.
- This change streamlines the setup process for developers working with CEF integration, ensuring a smoother workflow.

* feat(whatsapp): enhance message ingestion and IndexedDB integration

- Introduced a new `IngestMessage` interface to standardize message structure for WhatsApp.
- Updated `IngestPayload` to include additional fields for better message handling, including `provider`, `chatId`, and `day`.
- Implemented a new function `persistWhatsappChatDay` to handle the ingestion of chat messages by day, improving data organization and retrieval.
- Enhanced the WhatsApp recipe to utilize IndexedDB for direct data access, eliminating the need for DOM scraping and improving performance.
- Updated the Tauri configuration to enable development tools for easier debugging of webview accounts.

This commit significantly improves the application's ability to manage and ingest WhatsApp messages, providing a more robust and efficient user experience.

* feat(cdp): integrate IndexedDB scanner for WhatsApp via Chrome DevTools Protocol

- Added a new module for scanning IndexedDB using the Chrome DevTools Protocol (CDP), enabling direct access to WhatsApp data without DOM scraping.
- Implemented a scanner that communicates with the embedded CEF instance to read and decrypt messages stored in IndexedDB.
- Updated the Tauri application to manage the new scanner, ensuring it operates seamlessly with existing webview accounts.
- Enhanced the Cargo.toml and Cargo.lock files to include necessary dependencies such as `tokio-tungstenite` and `futures-util` for asynchronous operations.
- Refactored the WhatsApp recipe to utilize the new scanning capabilities, improving performance and data handling.

This commit significantly enhances the application's ability to interact with WhatsApp's IndexedDB, providing a more efficient and robust user experience.

* feat(cdp): enhance message diagnostics in IndexedDB scanner

- Updated the ScanSnapshot struct to include new fields for message diagnostics: `messageKeyUnion`, `messageTypeBreakdown`, and `sampleByType`, providing a comprehensive overview of message structures and types.
- Modified the scanner logic to capture and log detailed information about message types and their shapes, improving debugging capabilities.
- Refactored the JavaScript scanner to aggregate message key signatures and counts, enhancing the analysis of message records.

This commit significantly improves the application's ability to analyze and log message data from WhatsApp's IndexedDB, facilitating better debugging and data handling.

* feat(cdp): implement fast DOM scraping and crypto key extraction for WhatsApp

- Introduced a new fast-tick DOM scraping mechanism to extract rendered WhatsApp message bodies, enabling near real-time message updates without relying on IndexedDB.
- Added scripts for capturing and logging CryptoKey operations within WhatsApp's workers, allowing for better analysis of key derivations and decryptions.
- Enhanced the CDP scanner to interleave fast DOM scans with full IndexedDB scans, optimizing data retrieval and reducing UI spamming during idle periods.
- Updated the ScanSnapshot struct to include new fields for DOM-scraped messages and crypto operation statistics, improving the overall diagnostic capabilities of the application.

This commit significantly enhances the application's ability to interact with WhatsApp's messaging system, providing a more efficient and responsive user experience.

* feat(whatsapp): replace cdp_indexeddb with whatsapp_scanner for enhanced message handling

- Replaced the `cdp_indexeddb` module with `whatsapp_scanner` to streamline the scanning process for WhatsApp messages.
- Updated the application to manage the new `ScannerRegistry` for WhatsApp, improving the integration with the Chrome DevTools Protocol.
- Introduced new scripts for fast DOM scraping and full IndexedDB scanning, optimizing data retrieval and enhancing real-time message updates.
- Added a new `dom_scan.js` for efficient extraction of rendered message bodies directly from the DOM, reducing reliance on IndexedDB.
- Enhanced the `ScanSnapshot` struct to accommodate new fields for DOM-scraped messages, improving diagnostic capabilities.

This commit significantly improves the application's ability to interact with WhatsApp, providing a more efficient and responsive user experience.

* refactor(whatsapp): replace JavaScript DOM scanning with Rust-based DOM snapshot

- Removed the `dom_scan.js` script and replaced it with a new Rust module `dom_snapshot.rs` that captures DOM snapshots directly via the Chrome DevTools Protocol, enhancing performance and reliability.
- Introduced a new `idb.rs` module for scanning WhatsApp's IndexedDB, streamlining data retrieval and improving integration with the Rust backend.
- Updated the `ScanSnapshot` struct to accommodate changes in data handling, ensuring compatibility with the new scanning methods.
- Enhanced overall message handling capabilities, providing a more efficient and responsive user experience.

This commit significantly improves the application's ability to interact with WhatsApp, leveraging Rust for better performance and reducing reliance on JavaScript for DOM operations.

* feat(docs): add webview integration playbook for third-party messaging

- Introduced a comprehensive playbook detailing the process for integrating third-party webviews (e.g., Instagram, Messenger) into the application.
- Documented architecture, workflow, and best practices for building and debugging new integrations, leveraging Rust and Chrome DevTools Protocol.
- Included step-by-step instructions for setting up scanners, monitoring logs, and optimizing message handling, ensuring a streamlined development experience for future integrations.

This addition enhances the documentation, providing developers with a clear guide to implement and maintain webview integrations effectively.

* docs(webview): improve table formatting for clarity

- Enhanced the formatting of tables in the webview integration playbook to improve readability and consistency.
- Adjusted column headers and alignment for better presentation of job intervals and costs, ensuring clearer communication of scanning processes and common pitfalls.

This update aims to provide a more user-friendly documentation experience for developers integrating third-party webviews.

* feat(slack): integrate Slack scanner for message extraction and management

- Updated the OpenHuman package version to 0.52.15 in both Cargo.lock files.
- Introduced a new Slack scanner module to extract messages, users, and channels from Slack's IndexedDB using the Chrome DevTools Protocol.
- Added functionality to manage Slack accounts within the application, allowing for automatic opening of Slack webviews based on environment variables.
- Enhanced the existing webview account management to support Slack integration, ensuring seamless interaction with the Slack API.

This commit significantly improves the application's ability to interact with Slack, providing a robust framework for message handling and account management.

* fix(slack): one-doc-per-channel + omit CDP indexName param

CEF 146's IndexedDB.requestData rejects `indexName: ""` with "Could not
get index"; the CDP spec says empty string means the primary-key index
but this backend only accepts the field unset. Omit it entirely so the
Slack Redux-persist dump actually comes back.

Also switch memory grouping from (channel, day) → channel. Each Slack
channel is now one long-running memory doc keyed by channel name
(e.g. `general`, `team-product`, `elvin516`), falling back to channel
id for non-slug names. Every transcript line carries its own
`YYYY-MM-DD HH:MM` stamp and the header records the full date range.

`infer_team_id` updated to Slack's real DB naming pattern
`objectStore-<TEAM>-<USER>` (not `ReduxPersistIDB:` as initially
assumed).

* fix(onboarding): update navigation and test descriptions in OnboardingOverlay tests

- Changed the navigation path from '/conversations' to '/chat' in the OnboardingOverlay tests to reflect the updated routing logic.
- Updated test descriptions for clarity, ensuring they accurately describe the functionality being tested.

These changes enhance the accuracy and readability of the onboarding tests, aligning them with the current application flow.

* fix(conversations): add return statement to Conversations component

- Introduced a return statement in the Conversations component to ensure proper rendering of the sidebar or page variant.
- This change enhances the component's functionality by ensuring it returns the expected JSX structure.

These modifications improve the overall structure and behavior of the Conversations component.

* feat(accounts): add Discord integration and enhance AddAccountModal

- Introduced Discord as a new account provider, including its icon and service details.
- Updated the AddAccountModal to filter out already connected providers, improving user experience.
- Enhanced the UI to display a message when all providers are connected, ensuring clarity for users.
- Implemented context menu functionality for account management, allowing users to log out directly from the accounts list.

These changes expand the application's capabilities by integrating Discord and refining account management features.

* feat(discord): integrate Discord scanner for HTTP and WebSocket monitoring

- Added a new `discord_scanner` module to capture Discord API calls and WebSocket frames using the Chrome DevTools Protocol (CDP).
- Updated the `lib.rs` to manage the new Discord scanner alongside existing WhatsApp and Slack scanners.
- Enhanced the `webview_accounts` module to support Discord account management, including scanner registration and cleanup.

These changes expand the application's capabilities by enabling real-time monitoring of Discord interactions, enhancing user experience and functionality.

* feat(google-meet): integrate Google Meet as a new account provider

- Added Google Meet as a supported account provider, including its icon and service details.
- Updated the account management logic to handle Google Meet interactions, including recipe integration for call monitoring and notifications.
- Enhanced the UI to accommodate the new provider, ensuring a seamless user experience when managing accounts.

These changes expand the application's capabilities by integrating Google Meet, allowing users to join calls and receive notifications directly within the app.

* refactor(runtime): remove notification handling and composer autocomplete

- Eliminated the notification interception logic and associated functions, streamlining the runtime code.
- Removed composer autocomplete features, transferring responsibility for ghost-text overlays to the UI host.
- Updated comments to reflect the changes and clarify the remaining functionality.

These modifications simplify the runtime script, focusing on core features while delegating UI responsibilities.

* feat(google-meet): enhance Google Meet integration with lifecycle event handling

- Implemented lifecycle event handling for Google Meet, including events for call start, captions, and call end.
- Introduced in-memory storage for caption snapshots during meetings, allowing for the generation of markdown transcripts upon call completion.
- Added interfaces for payload structures related to Google Meet events, improving type safety and clarity in the codebase.
- Updated the webview account service to manage active meetings and flush transcripts to memory, ensuring a seamless user experience.

These changes significantly enhance the Google Meet integration, enabling real-time caption handling and transcript generation, thereby improving the overall functionality of the application.

* feat(cef): integrate CEF-based notification handling and update dependencies

- Added support for native OS notifications through the `tauri-runtime-cef` crate, enabling interception of browser notifications in embedded webviews.
- Introduced a new submodule for `tauri-cef` to manage CEF dependencies and facilitate notification handling.
- Updated the `.gitignore` to exclude CEF-related build artifacts and lock files.
- Removed the deprecated `notification_scanner` module, streamlining the codebase and focusing on the new CEF integration.
- Enhanced the `webview_accounts` module to register and manage CEF browser notifications, improving user experience with real-time alerts.

These changes significantly enhance the application's notification capabilities, leveraging CEF for a more integrated and responsive user experience.

* feat(cef): update CEF integration and dependency management

- Added `cef` and `tauri-runtime-cef` as dependencies to enhance CEF support.
- Updated `Cargo.toml` to reference `tauri-runtime-cef` from a local path, ensuring proper integration with the vendored CEF submodule.
- Removed direct Git references for Tauri packages, streamlining dependency management by using local paths.

These changes improve the application's CEF capabilities and simplify the dependency structure, facilitating better integration and maintenance.

* feat(browserscan): add BrowserScan as a new account provider with associated resources

- Introduced BrowserScan as a development-only account provider, including its icon and service details.
- Updated the account provider types and management logic to accommodate BrowserScan, enhancing the application's capabilities.
- Added a new recipe and manifest for BrowserScan, ensuring it integrates seamlessly into the existing webview account lifecycle.
- Enhanced the UI to display BrowserScan, providing users with a bot-detection sandbox for testing purposes.

These changes expand the application's functionality by integrating BrowserScan, allowing for improved testing and development workflows.

* feat(google-meet): enhance Google Meet integration with new transcript handling and session recovery

- Introduced a new service to handle Google Meet transcripts, enabling structured note extraction and proactive follow-up actions.
- Implemented session recovery logic to manage in-progress meetings when navigating away from the call.
- Updated the webview account service to log call events and captions, improving monitoring and debugging capabilities.
- Enhanced the Google Meet recipe to persist meeting state across navigations, ensuring seamless user experience.

These changes significantly improve the Google Meet integration, allowing for better management of meeting transcripts and user interactions.

* refactor(App): reorganize imports and clean up code structure

- Removed unused imports from App.tsx to streamline the code.
- Adjusted the import order for better readability and consistency.
- Enhanced the BottomTabBar component by simplifying the button rendering logic.
- Cleaned up the AddAccountModal component by consolidating prop destructuring.
- Improved formatting in various components for better code clarity.

These changes enhance code maintainability and readability across the application.

* update tauri

* feat(google-meet): improve caption handling and speaker identification logic

- Enhanced the logic for extracting speaker names from Google Meet rows, adding checks to filter out icon ligatures and irrelevant text.
- Updated the caption processing to better identify and score caption regions, ensuring more accurate transcript generation.
- Introduced new utility functions to differentiate between real captions and icon names, improving the overall reliability of the captioning feature.

These changes significantly enhance the accuracy and usability of the Google Meet integration, providing users with clearer and more relevant caption data.

* build(cef): bump vendor/tauri-cef to include full cef-helper source tree (#630)

Picks up 1b58f715 which fixes the bundler to copy the entire cef-helper/src/
tree instead of only main.rs — required for our CEF helper's Web Notifications
interception to link properly in downstream consumers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* build(cef): patch cef-dll-sys to fix/146-location-windows (#630)

The vendor tauri-cef workspace pins cef-dll-sys to the
fix/146-location-windows branch via its own [patch.crates-io], but cargo patches
do not propagate through path dependencies. Without pinning cef-dll-sys here
too, helper processes crash with `CefApp_0_CToCpp called with invalid version
-1` because the app-side bindings target a different CEF ABI than what the
vendor's cef-helper was built against.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(telegram): scaffold CDP-driven Telegram Web K scanner module (#630)

New telegram_scanner module mirrors the Slack/WhatsApp scanner shape from
PR #629 but targets Telegram Web K's IndexedDB surface via CDP:

- mod.rs: per-account poller + ScannerRegistry; connects to CDP on
  127.0.0.1:9222, picks the Telegram target, and runs an IDB tick every 30s.
  Emits webview:event and POSTs openhuman.memory_doc_ingest so memory fills
  even when the main window is hidden.
- idb.rs: IndexedDB walker — requestDatabaseNames / requestDatabase /
  requestData, with record caps per store.
- extract.rs: peer-grouped message/user/chat extraction from the `tweb`
  snapshot.

cef-only (wry has no remote-debugging port).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(telegram): register scanner registry and dev-auto env (#630)

Wires telegram_scanner into the Tauri builder:
- Registers ScannerRegistry as managed state (cef-only).
- Adds OPENHUMAN_DEV_AUTO_TELEGRAM=<uuid> helper mirroring the Slack /
  Google Meet dev-auto flow — opens the Telegram Web K account webview
  2s after startup so the CDP scanner has a target without manual UI
  clicks. Useful for iterating on the scanner end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(telegram): dispatch scanner on telegram account open/close/purge (#630)

Mirrors the slack/discord branches in webview_accounts:
- open(provider="telegram"): look up the telegram ScannerRegistry and
  ensure a CDP scanner is running for this account.
- close / purge: forget the account's scanner entry alongside the other
  providers so we don't leak poll loops.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* style: apply pre-push formatting

* fix(telegram): bind CDP scanner to account-marked targets

---------

Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 13:58:06 -07:00
cb455e807d feat(channels): register iMessage in the channel registry (local-only) (#725)
* feat(imessage): add chat.db scanner that ingests into memory_doc_ingest

Adds a macOS-only Tauri-side scanner at
`app/src-tauri/src/imessage_scanner/` that reads
`~/Library/Messages/chat.db` read-only on a 60s tick, groups messages
by `(chat_identifier, day)`, and posts one
`openhuman.memory_doc_ingest` JSON-RPC call per group — matching the
convention documented in `docs/webview-integration-playbook.md` and
used by the WhatsApp scanner.

This is the first local-native integration to follow the memory-doc
convention (webview sources like WhatsApp already do). No CEF / CDP /
DOM / IDB needed — iMessage persists everything locally in SQLite, so
a single-tick scanner is sufficient.

Changes:
- New module `imessage_scanner/{mod,chatdb}.rs` with unit tests.
- `lib.rs`: register `ScannerRegistry` and spawn on setup (macOS only).
- `Cargo.toml`: add `anyhow`, `parking_lot`, `chrono`, and
  macOS-only `rusqlite` (bundled).

Feature-gated to `target_os = "macos"`; non-macOS builds get a no-op
stub. Requires Full Disk Access to read chat.db — the read_since
helper surfaces a clear error pointing at System Settings on
permission failure.

* test(imessage): add real chat.db integration tests (ignored by default)

Two tests validating against the actual ~/Library/Messages/chat.db:

- real_chat_db_opens_and_returns_messages — confirms the SQL JOIN is
  compatible with macOS's actual schema, rusqlite bindings deserialize
  rows into our Message struct, and Full Disk Access errors surface
  cleanly when denied.
- real_chat_db_empty_past_cursor — confirms cursor semantics (rows
  past i64::MAX-1 return empty).

Both gated with #[ignore] so CI remains green without FDA. Run with:

    cargo test --manifest-path app/src-tauri/Cargo.toml \
        --lib imessage_scanner -- --ignored

Verified locally against a 463K-message chat.db: both pass.

* fix(imessage): address CodeRabbit review — data loss, upsert correctness, client reuse, local TZ

Four fixes stacked on top of the scanner from the previous commit:

1. Data-loss bug (major): newer macOS versions store the body in
   attributedBody (NSKeyedArchiver/typedstream blob) with text = NULL.
   chatdb now fetches attributedBody alongside text. format_transcript
   falls back to a heuristic extractor (longest printable-ASCII run,
   skipping known typedstream type markers) when text is absent. This
   recovers the plain-text body on macOS >= 11 without pulling in a
   full typedstream decoder. Emoji / non-Latin glyphs in attributedBody
   are deferred (follow-up).

2. Correctness bug (major): per-tick groups were ingested as partial
   deltas but keyed on (chat, day) — each tick\'s upsert replaced the
   full-day transcript with just the minute\'s messages. Now on each
   tick we collect unique (chat, day) keys touched by the new rows,
   call chatdb::read_chat_day() to fetch the complete day slice, and
   post the full transcript. Upsert is idempotent; re-runs after a
   crash reproduce the same document.

3. Cursor persistence: last_rowid is now read from / written to a file
   under the Tauri app-data dir (per account) so a restart doesn\'t
   replay the whole 30-day backfill.

4. Nits: reqwest::Client is shared via OnceLock (one TLS/pool init),
   and day grouping uses chrono::Local so the user-facing (chat, day)
   keys line up with the user\'s calendar day instead of UTC.

Tests: 6 unit tests green (added extractor + message_body + local-tz
coverage); 2 real-db integration tests (ignored by default) still pass
against the 463k-message chat.db on my machine.

* fix(imessage): gate scanner startup on feature="cef" to match module decl

Module declaration is #[cfg(feature = "cef")], so the macOS setup block
that references imessage_scanner::ScannerRegistry must carry the same
feature gate or wry builds fail to resolve the symbol.

* feat(channels): register iMessage channel with local-only auth + config persistence

iMessage already has the AppleScript send/receive bridge in channels/providers/imessage.rs
and the startup wiring in channels/runtime/startup.rs, but was missing from the public
channel registry, so the UI can't list or connect it and `connect_channel` returns
"unknown channel: imessage".

This adds:
- imessage_definition() in the channel registry with ManagedDm auth mode and an
  optional allowed_contacts field (comma-separated phone numbers / emails; * for any)
- Short-circuit branch in connect_channel: iMessage has no credentials to store, so
  it persists channels_config.imessage (IMessageConfig { allowed_contacts }) and
  returns connected without calling store_provider_credentials (which rejects
  empty credential payloads)
- Matching branch in disconnect_channel: skips remove_provider_credentials and
  clears channels_config.imessage
- Three unit tests: contacts persisted, empty contacts allowed, disconnect clears

Capabilities: SendText + ReceiveText. No SendRichText (AppleScript bridge is
plaintext only).

Test plan:
- cargo test -p openhuman --lib channels::controllers → 66 passed
- Still need GUI end-to-end with FDA granted; covered in follow-up.

* fix(imessage-scanner): address CodeRabbit review (PR #725)

- chatdb: filter queries to `service = 'iMessage'` so SMS/MMS rows
  in chat.db are never ingested into the iMessage channel.
- scanner: gate every tick on `channels_config.imessage` via the
  existing `openhuman.config_get` JSON-RPC — no ingestion before
  the user connects, and scanning stops as soon as they disconnect.
- scanner: apply the configured `allowed_contacts` allowlist before
  rebuilding/ingesting each chat (`*` or empty list means allow all).
- scanner: keep the cursor pinned when a full-day read or memory
  write fails, so a transient core outage no longer permanently
  skips messages.
- tests: add `chat_allowed` coverage for empty list, wildcard, and
  case-insensitive exact match.

---------

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
2026-04-21 13:24:50 -07:00
b54c213aec feat(imessage): chat.db scanner into memory_doc_ingest (#724)
* feat(imessage): add chat.db scanner that ingests into memory_doc_ingest

Adds a macOS-only Tauri-side scanner at
`app/src-tauri/src/imessage_scanner/` that reads
`~/Library/Messages/chat.db` read-only on a 60s tick, groups messages
by `(chat_identifier, day)`, and posts one
`openhuman.memory_doc_ingest` JSON-RPC call per group — matching the
convention documented in `docs/webview-integration-playbook.md` and
used by the WhatsApp scanner.

This is the first local-native integration to follow the memory-doc
convention (webview sources like WhatsApp already do). No CEF / CDP /
DOM / IDB needed — iMessage persists everything locally in SQLite, so
a single-tick scanner is sufficient.

Changes:
- New module `imessage_scanner/{mod,chatdb}.rs` with unit tests.
- `lib.rs`: register `ScannerRegistry` and spawn on setup (macOS only).
- `Cargo.toml`: add `anyhow`, `parking_lot`, `chrono`, and
  macOS-only `rusqlite` (bundled).

Feature-gated to `target_os = "macos"`; non-macOS builds get a no-op
stub. Requires Full Disk Access to read chat.db — the read_since
helper surfaces a clear error pointing at System Settings on
permission failure.

* test(imessage): add real chat.db integration tests (ignored by default)

Two tests validating against the actual ~/Library/Messages/chat.db:

- real_chat_db_opens_and_returns_messages — confirms the SQL JOIN is
  compatible with macOS's actual schema, rusqlite bindings deserialize
  rows into our Message struct, and Full Disk Access errors surface
  cleanly when denied.
- real_chat_db_empty_past_cursor — confirms cursor semantics (rows
  past i64::MAX-1 return empty).

Both gated with #[ignore] so CI remains green without FDA. Run with:

    cargo test --manifest-path app/src-tauri/Cargo.toml \
        --lib imessage_scanner -- --ignored

Verified locally against a 463K-message chat.db: both pass.

* fix(imessage): address CodeRabbit review — data loss, upsert correctness, client reuse, local TZ

Four fixes stacked on top of the scanner from the previous commit:

1. Data-loss bug (major): newer macOS versions store the body in
   attributedBody (NSKeyedArchiver/typedstream blob) with text = NULL.
   chatdb now fetches attributedBody alongside text. format_transcript
   falls back to a heuristic extractor (longest printable-ASCII run,
   skipping known typedstream type markers) when text is absent. This
   recovers the plain-text body on macOS >= 11 without pulling in a
   full typedstream decoder. Emoji / non-Latin glyphs in attributedBody
   are deferred (follow-up).

2. Correctness bug (major): per-tick groups were ingested as partial
   deltas but keyed on (chat, day) — each tick\'s upsert replaced the
   full-day transcript with just the minute\'s messages. Now on each
   tick we collect unique (chat, day) keys touched by the new rows,
   call chatdb::read_chat_day() to fetch the complete day slice, and
   post the full transcript. Upsert is idempotent; re-runs after a
   crash reproduce the same document.

3. Cursor persistence: last_rowid is now read from / written to a file
   under the Tauri app-data dir (per account) so a restart doesn\'t
   replay the whole 30-day backfill.

4. Nits: reqwest::Client is shared via OnceLock (one TLS/pool init),
   and day grouping uses chrono::Local so the user-facing (chat, day)
   keys line up with the user\'s calendar day instead of UTC.

Tests: 6 unit tests green (added extractor + message_body + local-tz
coverage); 2 real-db integration tests (ignored by default) still pass
against the 463k-message chat.db on my machine.

* fix(imessage): gate scanner startup on feature="cef" to match module decl

Module declaration is #[cfg(feature = "cef")], so the macOS setup block
that references imessage_scanner::ScannerRegistry must carry the same
feature gate or wry builds fail to resolve the symbol.

---------

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
2026-04-21 11:20:33 -07:00
3da80d852e feat(skills,node): integrate SKILL.md + managed Node runtime (#681) (#723)
* build(skills): add serde_yaml for SKILL.md frontmatter (#681)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(skills): parse SKILL.md frontmatter and add multi-path discovery (#681)

Extends the skills module with agentskills.io-style discovery:

* Parses YAML frontmatter in SKILL.md (name, description, version,
  author, tags, allowed-tools, license) via serde_yaml, with a
  catch-all extras map for forward-compatible keys.
* Adds SkillScope (User / Project / Legacy) and discover_skills(),
  which scans ~/.openhuman/skills, ~/.agents/skills, <ws>/.openhuman/
  skills, <ws>/.agents/skills and the legacy <ws>/skills directory.
* Gates project-scope loading behind an explicit
  <ws>/.openhuman/trust marker (is_workspace_trusted helper).
* Resolves name collisions with project > user > legacy precedence
  and surfaces shadowing as per-skill warnings.
* Inventories bundled resources under scripts/, references/, assets/.
* Keeps the existing load_skills(workspace_dir) API as a
  backwards-compatible wrapper for existing callers.
* Preserves legacy skill.json fallback (marked legacy = true).
* Lenient validation: missing / mismatched / oversized name or
  description produce warnings instead of errors.
* Adds 12 unit tests covering frontmatter parsing, trust gating,
  collision shadowing, lenient fallbacks, legacy JSON and resource
  inventory. One existing prompt test updated to use
  ..Default::default() for the expanded struct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(skills): align frontmatter with agentskills.io spec (#681)

Per the agentskills.io SKILL.md spec, only name, description, license,
compatibility, metadata, and allowed-tools are valid top-level keys.
Our SkillFrontmatter had version, author, and tags as top-level fields,
which drifted from the spec and would silently swallow mis-shaped data
for skills authored against the canonical schema.

Demote version/author/tags into the metadata map. Non-spec top-level
keys still parse via #[serde(flatten)] into extra, and when present
there we emit a migration warning and still populate the derived Skill
fields so existing skills keep working. Add compatibility as an
optional string alongside license.

New tests cover the spec-compliant metadata shape, the deprecated
top-level fallback path, and verify that the full spec frontmatter
parses without leaking into extras.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* build(runtime): add tar+xz2+zip for node runtime extraction (#681)

Node.js distributions ship as .tar.xz on Unix and .zip on Windows. The
upcoming Node runtime bootstrap extracts these in pure Rust; xz2 is
built with the `static` feature so liblzma is bundled and not a system
dependency. zip is pinned to default-features = false + deflate to
avoid pulling in bzip2/zstd/aes which we don't need for Node archives.

Deps only — no behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(config): add NodeConfig with env overrides (#681)

Introduce managed Node.js runtime configuration so skills that require
`node`/`npm` (agentskills.io packages with build steps) can be wired in
behind a single config switch. Fields:

- `node.enabled` — master switch (default true)
- `node.version` — pinned release (default `v22.11.0` LTS)
- `node.cache_dir` — absolute path for managed distributions; empty = use
  workspace default
- `node.prefer_system` — reuse matching system `node` when found (default
  true); set false for reproducible CI / airgapped deploys

Env overrides land in load.rs alongside the existing LOCAL_AI_TIER block:
- OPENHUMAN_NODE_ENABLED
- OPENHUMAN_NODE_VERSION
- OPENHUMAN_NODE_CACHE_DIR
- OPENHUMAN_NODE_PREFER_SYSTEM

No resolver / downloader yet — subsequent commits in the #681 series
consume this config to bootstrap the runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(node_runtime): detect compatible system node on PATH (#681)

Introduce the `openhuman::node_runtime` module and its first piece: a
synchronous resolver that walks `PATH`, probes `node --version`, and
returns a `SystemNode` when the host toolchain major-version matches the
configured target.

Why resolve first: a successful probe lets the bootstrap skip a
~60 MB download per managed install. Matching is intentionally loose on
the patch level (Node LTS lines are ABI-stable and skills pin their own
deps via package-lock.json); operators needing strict pinning can set
`node.prefer_system = false`.

Tracing is verbose by design — resolver decisions gate the download
path, so operators need a clear breadcrumb trail at `debug`/`info`.

Includes unit tests for `parse_node_version` covering `v` prefix,
whitespace, major-only, and malformed inputs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(node_runtime): SHASUMS256-verified distribution downloader (#681)

Add the second piece of the managed Node.js runtime: streaming download
of OS/arch-specific prebuilt archives from nodejs.org, gated by a
SHA-256 match against the release's signed `SHASUMS256.txt`.

Key contracts:

- `NodeDistribution::for_host(version)` picks the right archive for the
  current OS/arch tuple. Supported matrix:
  * darwin-{arm64,x64}.tar.xz
  * linux-{arm64,x64,armv7l}.tar.xz
  * win-{arm64,x64}.zip
  Unsupported hosts surface a clear error so the caller can flip
  `node.enabled = false` or point at a pre-installed toolchain.

- `fetch_shasums(client, version)` parses `SHASUMS256.txt` into a
  filename -> hex digest map. Tolerant of trailing / signature blocks.

- `download_distribution(...)` streams chunks into the target path,
  computes SHA-256 on the fly, and wipes the partial file if the
  digest does not match. Integrity check is mandatory — skills will
  execute untrusted code inside the resolved runtime.

Verbose tracing at `info` and `debug` follows the repo debug-logging
rule; operators can grep `[node_runtime::downloader]` to trace every
GET, chunk boundary (via total-bytes log), and hash decision.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(node_runtime): archive extraction + atomic install (#681)

Extract downloaded Node.js distributions and move them into the final
cache path without leaving the reader observing a half-populated
directory.

- `extract_distribution(archive, extract_root, is_zip)` — wraps the
  synchronous `tar`/`xz2`/`zip` crates in `spawn_blocking` and returns
  the single top-level folder produced by the archive (`node-vX.Y.Z-
  <os>-<arch>/`). `set_preserve_permissions(true)` + `set_overwrite(
  true)` on the tar side keeps the `node` binary's `+x` bit. The zip
  path restores Unix mode bits via `unix_mode()` so cross-OS builds
  still land correct permissions.
- `atomic_install(staged, final_dest)` — renames a staged directory
  into place via a single `rename(2)`, moving any pre-existing install
  to a `.old-<pid>` sibling first and cleaning it up on success. This
  gives concurrent readers a "before" or "after" view, never a
  partial one.
- Unsafe zip paths (`enclosed_name()` rejection) are logged and
  skipped rather than traversed — defence against malicious archives,
  even though the digest check in the downloader already rules out
  tampered official releases.

No new tests here: the logic leans on upstream tar/zip crates, and
meaningful end-to-end coverage requires a real archive on disk — that
comes in the integration-test commit later in this series.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(node_runtime): bootstrap mutex + cache + bin path helper (#681)

Stitch resolver, downloader, and extractor into a single idempotent
entry point callers use at startup (or lazily before the first
`node_exec`/`npm_exec` call).

`NodeBootstrap::resolve()` contract:
1. Return cached `ResolvedNode` if a previous call already succeeded.
2. Bail when `node.enabled = false`.
3. If `node.prefer_system`, probe the host PATH. Matching major
   version wins — return a `System`-sourced `ResolvedNode`.
4. Otherwise compute the install path
   (`{cache_dir}/node-v{version}-{os}-{arch}/`). If it already contains
   valid bins, reuse it. This makes the bootstrap ~free across
   restarts once a managed install lands.
5. Else fetch `SHASUMS256.txt`, locate the expected digest for our
   archive, stream the download, extract into a `.stage-<pid>` scratch
   dir, `atomic_install` the top-level folder into the final path, and
   remove scratch + archive to reclaim disk.

Concurrency is handled by a `tokio::sync::Mutex<Option<ResolvedNode>>`
— parallel callers queue behind the first one; the winner memoises,
the losers pick up the cached result. No race can produce two
concurrent downloads of the same archive.

Platform-specific bin layout lives in `managed_bin_dir` / `build_
resolved`:
- Unix: `<install>/bin/{node,npm}`
- Windows: `<install>/{node.exe,npm.cmd}`

This closes the resolver/downloader/cache layer promised in the #681
checkpoint. Tools (`node_exec`, `npm_exec`) and shell-PATH injection
layer on top in the next batch of commits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tools): node_exec runs JS via managed node runtime (#681)

Executes `inline_code` (via `node -e`) or a workspace-relative
`script_path` through the NodeBootstrap-resolved `node` binary. POSIX
single-quote quoting keeps user input inert; `env_clear` + allow-list
mirrors the shell tool so secrets never leak. 300s default timeout
(capped at 1800s), 1MB stdout/stderr caps. PATH is prepended with the
resolved bin dir so child processes see managed `node`/`npm`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(tools): npm_exec runs npm via managed node runtime (#681)

Thin npm CLI wrapper paired with node_exec. Subcommands go through
`is_sane_subcommand` (alphanumerics + `._-:` only) and a deny-list
(publish/adduser/login/token/…) blocks registry-mutation and auth
flows. `cwd` is resolved under the workspace — absolute paths and
`..` components are rejected. 600s default timeout (1800s ceiling),
1MB stdout/stderr caps. PATH prepended with managed bin dir so
npm's own node/corepack lookups hit the managed toolchain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(tools): shell prepends managed node bin to PATH when cached (#681)

Adds a non-blocking `NodeBootstrap::try_cached()` primitive that peeks the
memoised `ResolvedNode` without holding the async lock or triggering a
download. ShellTool gains an optional `node_bootstrap` field wired through
a new `with_node_bootstrap` constructor; when set, each shell invocation
consults `try_cached()` and, on a hit, prepends the managed bin dir to
the child PATH using the platform separator. Unrelated shell commands
stay byte-identical — no download is ever forced from the shell path.

Consequence: once `node_exec`/`npm_exec` have resolved the toolchain
once, skills that shell out to `node`/`npm`/`npx`/`corepack` transparently
pick up the managed install without any per-command coordination.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(tools): register node_exec + npm_exec behind node.enabled (#681)

Wires the skills-oriented Node.js toolchain into the tool registry:

* Constructs one session-scoped `NodeBootstrap` in `all_tools_with_runtime`
  when `root_config.node.enabled` is true — all three consumers (ShellTool,
  NodeExecTool, NpmExecTool) share the same `Arc<NodeBootstrap>` so the
  download/extract/install pipeline runs at most once per session and the
  memoised `ResolvedNode` is reused across every shelling-out call.
* Swaps ShellTool to `with_node_bootstrap(...)` when the bootstrap exists
  so shell PATH injection fires automatically once any node/npm tool has
  resolved the runtime.
* Registers `node_exec` and `npm_exec` only when the flag is on — flipping
  `node.enabled = false` cleanly removes both tools and falls back to the
  legacy ShellTool construction.
* Adds two regression tests (`all_tools_registers_node_exec_when_node_enabled`
  and `all_tools_excludes_node_exec_when_node_disabled`) so future refactors
  can't silently drop the wiring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(agents): grant node_exec + npm_exec to code_executor + tool_maker (#681)

code_executor is the sandboxed developer sub-agent — it writes, runs, and
debugs code — and tool_maker is the narrow self-healing agent that polyfills
missing commands. Both now see the managed Node.js tools so skills and
polyfills can call into the resolved runtime directly rather than relying on
whatever `node`/`npm` happens to be on the host PATH.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(core): apply cargo fmt auto-fixes (#681)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(cargo): sync Cargo.lock to OpenHuman v0.52.26 after rebase (#681)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(node_runtime): strip leading v/V defensively in resolve_from_system (#681)

CodeRabbit R1 flagged that build_resolved receives a raw version string from
SystemNode. detect_system_node already trims the prefix, but trim defensively
at the resolve_from_system boundary too so any future code path constructing
SystemNode with an un-normalised version won't emit "vvX.Y.Z".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(node_runtime): propagate flush errors and clean up partial archives (#681)

CodeRabbit R2 flagged that download_distribution silently swallowed flush
errors via `.ok()` and left partial files on disk when any chunk/write/flush
step errored. Wrap the streaming loop in an async block returning Result<()>,
propagate flush errors, and on any failure delete the partial archive so a
retry starts clean and callers never see a half-written file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(node_runtime): restore previous install on staged rename failure (#681)

CodeRabbit R3 flagged that atomic_install left the user with no Node runtime
on disk if the staged->final rename failed after a backup had been taken.
Wrap the rename in `if let Err`, restore the backup if present, log restore
failures separately (warning), and always return the original error so the
caller sees the true failure cause.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(node_runtime): enforce real 5s timeout on node --version probe (#681)

CodeRabbit R4 flagged that probe_node_version used Command::output() with no
real timeout — a broken shim or FUSE-backed binary could hang the bootstrap
forever. Add wait-timeout crate dep and rewrite the probe to spawn with
piped stdio, wait_timeout(5s), kill on timeout, and read stdout/stderr from
the piped handles after exit. Logs a warning when a probe times out so the
install can be diagnosed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tools): reject script_path escapes in node_exec (#681)

CodeRabbit R5 flagged that node_exec joined user-supplied script_path onto
the workspace without rejecting `..` segments or absolute paths, letting a
prompt-injected `../../../etc/passwd` read arbitrary files via node. Add a
resolve_script_path helper mirroring npm_exec::resolve_cwd — reject empty,
absolute, parent-dir, and Windows-prefix components. Extra positional args
stay opaque (shell-quoted, not path-checked) and get an explanatory comment
at the call site. Unit tests cover each rejection case plus the relative-
subdir happy path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(skills): drop dead SKILL.md branch in legacy manifest loader (#681)

CodeRabbit N1 flagged dead code: load_from_legacy_manifest only runs when
load_skill_dir already determined SKILL.md is absent, so the fallback that
re-checked for SKILL.md and its helper read_skill_md_description could
never execute. Simplify to a direct description/location decision and
delete the dead helper.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(config): extract parse_env_bool helper with warn on unknown values (#681)

CodeRabbit N2 flagged two near-identical boolean env-override match blocks
for OPENHUMAN_NODE_ENABLED and OPENHUMAN_NODE_PREFER_SYSTEM. Extract a
module-level parse_env_bool helper that accepts 1/true/yes/on and
0/false/no/off (case-insensitive) and emits a tracing::warn when the value
is unrecognised so silent mis-spellings don't invisibly leave the config
unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(node_runtime): detect corrupted cache missing npm (#723)

`probe_managed_install` now verifies the npm launcher exists before
reusing an extracted install. A download interrupted after `node` was
extracted but before `npm` would otherwise be cached forever and
`npm_exec` could never self-heal — now the corrupted cache forces a
fresh download via the normal resolve path.

Addresses CodeRabbit review feedback on PR #723.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(node_runtime): skip non-executable PATH shims when probing node (#723)

`which_node` previously returned the first matching filename on `PATH`
regardless of its execute bit. A non-executable `node` placeholder
earlier in `PATH` (e.g. an unprivileged shim left by a failed install)
would mask a valid later install and force the managed runtime download.

Now checks `file && (mode & 0o111 != 0)` on Unix to mirror shell `which`
behaviour. Windows remains unchanged — `.exe` suffix already encodes
executability for the loader.

Addresses CodeRabbit review feedback on PR #723.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(skills): deterministic entry order in scan_root (#723)

`read_dir` order is unspecified by the OS. When two sibling skill
directories declare the same logical `frontmatter.name` (which can
differ from the folder name), cross-scope/same-scope deduplication
downstream would pick a non-deterministic winner across runs.

Sort entries by on-disk directory name for a stable, reproducible
order so collision resolution is deterministic.

Addresses CodeRabbit review feedback on PR #723.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(skills): surface YAML parse errors as skill warnings (#723)

`parse_skill_md` previously swallowed the `serde_yaml` error via `log::warn!`
and fell back to an empty `SkillFrontmatter`, then the catalog reported a
generic "could not parse — exposing directory as placeholder". Skill
authors had no way to see the real cause without scraping logs.

Return parse-level diagnostics as a third tuple element. `load_from_skill_md`
merges them into the skill's user-visible `warnings`, so the catalog now
surfaces the actual YAML error (e.g. "frontmatter parse error: mapping
values are not allowed here at line 3 column 12").

Addresses CodeRabbit review feedback on PR #723.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(skills): skip symlinks when walking skill resources (#723)

`walk_files` used `is_dir()` / `is_file()` which transparently follow
symlinks. Two failure modes:

1. **Unbounded recursion** — a skill resource symlink that points back
   at an ancestor (e.g. `resources/self -> resources/`) would cause
   infinite traversal, eventually blowing the stack.
2. **Silent out-of-tree leakage** — a symlink pointing at `/`, `/etc`,
   or another skill's directory would enumerate its contents into the
   current skill's resource listing.

Switch to `entry.file_type()` and skip symlinks before descending.
Directories and regular files behave as before.

Addresses CodeRabbit review feedback on PR #723.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(node_runtime): default managed cache to user-owned OS cache (#723)

Default `cache_root()` to `dirs::cache_dir()/openhuman/node-runtime/` so a
repo cannot ship a checked-in `./node-runtime/` and have the bootstrap
reuse it as a trusted managed install. Explicit `config.cache_dir` still
wins; workspace-local falls back only when `dirs::cache_dir()` is
unavailable, and we emit a warning on that fallback.

As a second line of defence, `probe_managed_install()` now canonicalises
both the install dir and the cache root and refuses any install that
escapes the cache tree.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(node_runtime): require npm when accepting system node (#723)

On distros that package `node` and `npm` separately (Debian/Ubuntu,
Alpine `nodejs-current`, some NixOS setups) the host `node` can be
present without `npm`. `npm_exec` then fails every call because the
resolved `SystemNode` has no usable npm launcher.

Before returning `Some(SystemNode)`, locate `npm` on `PATH` and probe
`npm --version` through the same `wait_timeout` path as the node probe.
Either missing gate falls back to the managed download flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(skills): surface user-scope skills via load_skills (#723)

Existing production callers (`agent::harness::session::builder`,
`channels::runtime::startup`) reach the skill catalog via
`load_skills(workspace_dir)`. Previously this shim passed `None` for
the home directory, so skills installed under `~/.openhuman/skills/`
and `~/.agents/skills/` were silently dropped even after the
multi-scope discovery landed in `discover_skills`. Delegate to
`discover_skills_inner` with `dirs::home_dir()` so user-scope skills
reach the runtime; project-scope still wins on name collision.

Tests stay hermetic via a `load_skills_ws` helper that preserves the
old workspace-only semantics. A new `load_skills_surfaces_user_scope`
test drives a tempdir-as-home through `discover_skills` and asserts
the user-scope skill is returned with `SkillScope::User`.

Addresses CodeRabbit review comment 3116332244.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(skills): skip symlinked skill dirs during scan (#723)

`scan_root` previously accepted any entry where `path.is_dir()`
returned true. `is_dir()` dereferences symlinks, so a link from
`<skills-root>/foo -> /some/external/tree` would load as a
legitimate skill even though `walk_files` already rejects
symlinks deeper in the resource walker. Attacker-authored
symlinks in a skills root would therefore have one remaining
escape hatch.

Switch to `entry.file_type()` (non-dereferencing) and reject
both symlinked and non-directory entries at the top level.
Treat a failed `file_type()` probe as "not safe to traverse"
and skip.

A new `symlinked_skill_dirs_are_skipped` test (Unix-only, since
symlinks are the platform guarantee being tested) creates an
external skill tempdir, links it into the workspace skills root,
and asserts `load_skills_ws` returns empty.

Addresses CodeRabbit review comment 3116332252.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(skills): reject symlinked resource roots (#723)

`inventory_resources` gated each resource sub-root (`scripts/`,
`references/`, `assets/`) on `root.is_dir()`. `is_dir()` follows
symlinks, so a `scripts -> /etc` link inside a skill directory
would pass the check and `walk_files` would inventory the
external tree. Deeper symlinks inside the walk were already
rejected by `walk_files`, but the root-level check was the
missing layer.

Use `std::fs::symlink_metadata` for a non-dereferencing probe
and reject roots whose own `file_type().is_symlink()` returns
true. Treat any `symlink_metadata` error as a non-existent root
and skip.

A new `symlinked_resource_roots_are_rejected` test (Unix-only)
links a skill's `assets/` sub-root to an external tempdir with a
file inside and asserts `inventory_resources` returns empty.

Addresses CodeRabbit review comment 3116332260.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 10:59:21 -07:00
github-actions[bot] 5bc799ffeb chore(release): v0.52.27 v0.52.27 2026-04-21 14:57:32 +00:00