Commit Graph
515 Commits
Author SHA1 Message Date
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
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
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
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
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
Mega MindandGitHub 1792135aea fix(settings): tool capability toggles now persist and are enforced at runtime (#720)
* chore: update OpenHuman version to 0.52.26 and refine tool preference handling

- Bumped the OpenHuman package version to 0.52.26 in Cargo.lock files.
- Enhanced the ToolsPanel component to include user feedback on save status.
- Implemented filtering of tools based on user preferences in the Rust backend, allowing for more customizable tool availability.
- Added new utility functions to map UI tool IDs to Rust tool names for better integration.

These changes improve user experience and maintain compatibility with the latest features.

* style: apply cargo fmt to user_filter.rs
2026-04-20 13:50:02 -07:00
Steven EnamakelandGitHub ee3f6472ca feat: improve sub-agent tooling, conversation timeline UX, and Tauri setup (#646)
* refactor(tauri): update development scripts and configuration for CEF integration

- Modified `package.json` scripts to consistently export `CEF_PATH` for all `cargo tauri` commands, ensuring a unified CEF binary distribution location.
- Removed the overlay window configuration from `tauri.conf.json` and updated related Rust functions to reflect this change, while retaining helper functions for potential future use.
- Updated documentation in `install.md` to clarify the importance of setting `CEF_PATH` for consistent CEF integration across builds.
- Enhanced the `ensure-tauri-cli.sh` script to set `CEF_PATH` and ensure proper installation of the vendored CEF-aware `tauri-cli`.

These changes streamline the development workflow and improve the reliability of CEF integration in the application.

* refactor(release): enhance macOS signing script for nested frameworks and helper apps

- Introduced a new `codesign_hardened` function to streamline the signing process with consistent options.
- Improved the signing logic for nested frameworks and helper applications, ensuring all binaries are signed correctly.
- Updated output messages for better clarity during the signing process, including detailed listings of bundle contents.
- Disabled the summarizer payload threshold in the configuration to prevent recursive invocations until the issue is resolved.

These changes improve the reliability and maintainability of the macOS signing and notarization workflow.

* feat(orchestrator): add current_time tool and enhance agent capabilities

- Introduced the `current_time` tool to provide the current date and time in UTC and local time zones, facilitating scheduling and reminders.
- Updated `agent.toml` to include new tools: `current_time`, `cron_add`, `cron_list`, `cron_remove`, and `schedule`, enhancing the orchestrator's functionality.
- Expanded documentation in `prompt.md` to guide users on utilizing the new direct tools effectively.

These changes improve the orchestrator's ability to handle time-related queries and scheduling tasks directly, enhancing user experience.

* feat(gmail): implement post-processing for Gmail responses to convert HTML to markdown

- Added a new `post_process` module specifically for Gmail, which modifies action responses to convert HTML content into markdown format, improving usability and reducing context token usage.
- Enhanced the `ComposioProvider` trait with a `post_process_action_result` method to allow providers to handle response modifications.
- Introduced a `post_process` function that checks for a `raw_html` flag in the arguments to determine whether to apply the conversion.
- Implemented tests to validate the HTML detection and conversion logic, ensuring the integrity of the post-processing functionality.

These changes enhance the handling of Gmail responses, making them more suitable for further processing and display in the application.

* refactor(gmail): improve HTML to markdown conversion and tool result budget

- Enhanced the `post_process` module for Gmail to handle large HTML payloads more efficiently, implementing a fallback mechanism for oversized content.
- Updated the `DEFAULT_TOOL_RESULT_BUDGET_BYTES` to `0`, disabling the budget temporarily while reworking the oversized-output path.
- Refined the `extract_markdown_body` function to better manage HTML content, ensuring cleaner markdown output and improved performance.
- Added utility functions for stripping HTML noise and handling large email bodies, enhancing the overall robustness of the email processing logic.

These changes optimize the handling of Gmail responses, improving usability and performance in processing large HTML content.

* feat(thread): implement thread title generation from user and assistant messages

- Added a new `generateTitleIfNeeded` function in the `threadApi` to create a thread title based on the first user message and the assistant's reply.
- Introduced a new `GenerateConversationThreadTitleRequest` struct to handle requests for title generation.
- Updated the `ChatRuntimeProvider` to dispatch the title generation action after processing inference responses.
- Enhanced the `threadSlice` with a new async thunk for generating thread titles, ensuring proper error handling and thread loading.
- Added tests for the new title generation functionality to validate the integration with the threads RPC.

These changes improve the user experience by automatically generating relevant thread titles, enhancing the organization of conversations.

* feat(conversations): add thread title update functionality

- Implemented `update_thread_title` method in `ConversationStore` to allow updating the title of existing conversation threads.
- Added a corresponding public function `update_thread_title` for external access.
- Enhanced tests to verify that thread titles are correctly updated and persisted in the store.

These changes improve the management of conversation threads by enabling dynamic title updates, enhancing user experience and organization.

* feat(docs): add comprehensive agent and subagent tool flow documentation

- Introduced a new document detailing the runtime flow of the agent harness, including execution paths for main agents and tools.
- Explained the differences between typed and fork subagents, and provided guidance for debugging harness and delegation issues.
- Included a file map outlining key components and their roles within the Rust implementation.
- Added a flow diagram to visually represent the interaction between agents, tools, and subagents.

These changes enhance the understanding of the agent architecture and improve the documentation for developers working with the system.

* feat(subagent): implement extraction tool and handoff cache for oversized results

- Introduced `extract_from_result` tool to allow targeted queries against oversized tool outputs, improving efficiency by directly interacting with the extraction model.
- Added `ResultHandoffCache` to manage oversized payloads, enabling progressive disclosure and reducing context length issues in sub-agent history.
- Implemented hygiene helpers for cleaning tool outputs before caching, ensuring only relevant data is stored.
- Enhanced the sub-agent runner with new modules for tool preparation and execution, streamlining the overall agent workflow.

These changes enhance the sub-agent's ability to handle large tool results effectively, improving performance and user experience.

* feat(conversations): enhance message bubble rendering and timeline entry formatting

- Introduced new utility functions for parsing and rendering agent messages, including `splitAgentMessageIntoBubbles` and `parseMarkdownTable`, to improve the display of messages in conversation threads.
- Implemented `BubbleMarkdown` and `TableCellMarkdown` components for better formatting of user and agent messages, ensuring consistent styling and interaction.
- Enhanced the `formatTimelineEntry` function to provide clearer titles and details for tool timeline entries, improving the user experience during interactions with subagents.
- Updated the `ChatRuntimeProvider` to utilize the new formatting functions, ensuring that tool timeline entries are displayed with relevant context and detail.

These changes improve the overall presentation and usability of conversation messages and tool interactions, enhancing user engagement and clarity.

* feat(subagent): enforce restrictions on sub-agent spawning tools

- Introduced a filter to prevent sub-agents from invoking their own spawning tools, specifically `spawn_subagent` and `delegate_*`, to avoid recursion issues and ensure proper delegation by the top-level orchestrator.
- Updated the `is_subagent_spawn_tool` function to identify these tools and integrated checks in both `run_typed_mode` and `run_fork_mode` to maintain the integrity of the sub-agent execution environment.
- Enhanced logging to track the removal of restricted tools from the sub-agent's tool surface, improving observability and debugging capabilities.

These changes strengthen the sub-agent architecture by enforcing strict boundaries on tool invocation, enhancing stability and performance.

* feat(conversations): add ToolTimelineBlock component and enhance timeline entry formatting

- Introduced the `ToolTimelineBlock` component to display tool timeline entries with improved formatting and user interaction, including auto-expansion for running entries.
- Enhanced the `formatTimelineEntry` function to include user-friendly titles for specific tool actions, such as 'Viewing your Integrations'.
- Updated the rendering logic in the `Conversations` component to filter and display visible messages more effectively, improving user experience during conversations.

These changes enhance the clarity and usability of tool interactions within conversation threads, providing users with better context and engagement.

* refactor(conversations): streamline component imports and enhance formatting consistency

- Consolidated import statements in `Conversations.tsx` and `ChatRuntimeProvider.tsx` for improved readability.
- Refactored the `ToolTimelineBlock` component to simplify its props structure.
- Enhanced formatting consistency in the rendering logic of agent message bubbles and timeline entries, ensuring cleaner code and better maintainability.
- Updated test cases for `splitAgentMessageIntoBubbles` and `formatTimelineEntry` to reflect formatting changes and ensure accuracy.

These changes improve code clarity and maintainability while enhancing the overall user experience in conversation threads.

* fix: satisfy pre-push lint on fix/tauri

* fix(chat): refresh usage counters after responses

* refactor(ChatRuntimeProvider): remove redundant import of requestUsageRefresh

- Eliminated the duplicate import statement for `requestUsageRefresh` in `ChatRuntimeProvider.tsx`, streamlining the code for better readability and maintainability.

* fix(chat): satisfy pre-push checks

* refactor(conversations): improve error handling and remove unused title generation logic

- Removed the unused `generateThreadTitleIfNeeded` function call from the `Conversations` component, simplifying the message dispatch logic.
- Enhanced error handling during message dispatch by using `unwrap()` to catch and log errors, providing clearer feedback on send failures.
- Updated the `ChatRuntimeProvider` to ensure proper error logging when generating thread titles, improving observability of issues related to title generation.

These changes streamline the conversation handling process and improve the robustness of error management in the chat system.

* refactor(conversations): improve formatting of title redaction and streamline test assertions

- Reformatted the `redact_title_for_log` function for better readability by adjusting the formatting of the output string.
- Simplified the assertion in the timezone test to enhance clarity and maintainability.

These changes contribute to cleaner code and improved test structure in the conversations module.

* refactor(tokenjuice): sort fact parts for improved formatting in inline summary

- Modified the `format_inline` function to sort the fact parts before generating the summary string. This change enhances the readability and consistency of the output by ensuring that facts are presented in a stable order.

These changes contribute to better formatted summaries in the token juice module.

* fix: address remaining CodeRabbit review comments

* refactor: streamline error handling and improve code readability

- Updated error handling in prompt loading to use `std::io::Error::other` for better clarity.
- Simplified conditional checks using `is_none_or` and `is_some_and` for improved readability.
- Refactored string trimming logic to utilize array syntax for better clarity.
- Enhanced default implementations for several structs to reduce boilerplate code.

These changes contribute to cleaner code and improved maintainability across various modules.

* refactor: improve code formatting and structure

- Adjusted formatting in `post_process.rs` for better readability by aligning the conditional block.
- Combined derive attributes in `tools.rs` for the `ComputerControlConfig` struct to reduce redundancy.
- Streamlined entry retrieval in `compatible.rs` by condensing multiple lines into a single line for clarity.

These changes enhance code readability and maintainability across the affected modules.
2026-04-18 00:44:31 -07:00
Steven EnamakelandGitHub e5f571cec7 feat(tokenjuice): Rust port of terminal-output compaction engine (#644)
* feat(tokenjuice): implement core functionality for terminal-output compaction engine

- Introduced the `tokenjuice` module, which includes the classification and reduction of tool outputs based on JSON-configured rules.
- Added new dependencies for Unicode handling: `unicode-segmentation` and `unicode-width`.
- Implemented the `classify` module to match tool execution inputs against predefined rules, enhancing the ability to process and summarize terminal outputs.
- Created a comprehensive set of types and utilities for managing tool execution inputs and classification results.
- Established a built-in rule set for common tools, improving the initial setup and usability of the `tokenjuice` engine.
- Enhanced testing framework with integration tests to ensure the accuracy of output compaction and classification.

These changes lay the groundwork for a robust terminal-output management system, facilitating better interaction with various tools and improving overall user experience.

* feat(tokenjuice): implement tokenjuice module for terminal output compaction

- Introduced the `tokenjuice` module, which includes functionality for classifying and reducing terminal output based on JSON-configured rules.
- Added new dependencies: `unicode-segmentation` and `unicode-width` to support text processing.
- Created a new `classify.rs` file for rule classification logic, including matching helpers and scoring functions.
- Implemented a `reduce.rs` file to handle the main reduction pipeline and text normalization.
- Established a structured approach for loading and compiling rules from multiple sources, including built-in and user-defined rules.
- Added integration tests to ensure the correctness of the output reduction process.

These changes enhance the application's ability to manage and compact verbose tool outputs, improving overall efficiency and user experience.

* test(tokenjuice): enhance test coverage for classification and reduction logic

- Added a series of unit tests to `classify.rs` to validate the behavior of tool name filters and argument matching, ensuring correct classification of tool executions.
- Introduced tests for edge cases in `reduce.rs`, including command tokenization and normalization of execution inputs, to improve robustness against various input formats.
- Expanded tests in `builtin.rs` to cover duplicate ID reporting and compile issues, enhancing error handling and reporting mechanisms.
- Implemented additional tests in `compiler.rs` to verify regex handling in rule definitions, ensuring invalid patterns are correctly ignored.

These enhancements improve the overall test coverage and reliability of the tokenjuice module, facilitating better maintenance and future development.

* test(tokenjuice): add edge-case tests for gh table and reduction pipeline

* style(tokenjuice): apply cargo fmt

* feat(tokenjuice): wire into agent tool loop output compaction

Add `tokenjuice::compact_tool_output` helper and call it in the agent
tool loop after credential scrubbing (and on error paths with exit=1),
before any optional payload_summarizer. Derives argv/command
heuristically from JSON tool arguments (command / args / argv / cmd
shapes) so shell-wrapping tools still match upstream family rules
(git/*, package/*, tests/*, etc.). Pass-through safe: outputs under
512 bytes or where compaction saves <5% are returned untouched.

* fix(tokenjuice): address coderabbit review comments

- classify: derive command from argv join when input.command is unset,
  so commandIncludes* rules still match argv-only callers
- rules/loader: log read_dir / file_type / read_to_string failures at
  debug level so permission or filesystem issues are observable rather
  than silently skipped
- text/ansi: add trace log at strip_ansi entry/exit with lengths (no
  text content) per the project debug-logging rules
- tests: remove orphan src/openhuman/tokenjuice/tests/integration.rs
  which was never wired into any module declaration; the real fixture-
  parity runner lives at tests/tokenjuice_integration.rs and asserts
  hard when the fixtures directory is missing

Vendored-rule issues (docker-ps / kubectl-describe / git/branch /
grep casing / counter-pattern overbreadth / etc.) come from upstream
and are left as-is; this module is a straight port of the upstream
rule set and should not fork from it in v1.
2026-04-17 21:15:19 -07:00
Steven EnamakelandGitHub 8f4696bdfb feat(composio): per-toolkit tool curation, user scopes, and Gmail HTML→markdown (#643)
* feat(composio): add user scope management for toolkits

- Introduced `get_user_scopes` and `set_user_scopes` functions to manage per-toolkit user scope preferences, allowing for read, write, and admin classifications.
- Updated `all_controller_schemas` and `all_registered_controllers` to include new schemas for user scope management.
- Implemented `evaluate_tool_visibility` to determine tool visibility based on user-defined scopes, enhancing security and control over tool actions.
- Added `UserScopePref` struct to store user preferences and integrated it with memory storage for persistence.
- Enhanced existing tools to respect user scope preferences during execution, ensuring actions align with user-defined permissions.

These changes improve the flexibility and security of toolkit interactions, allowing users to customize their access levels for different actions.

* feat(gmail): expand GMAIL_CURATED tools for enhanced email management

- Updated the GMAIL_CURATED constant to include additional tools for reading and writing emails, such as GMAIL_LIST_MESSAGES, GMAIL_LIST_THREADS, GMAIL_GET_ATTACHMENT, and GMAIL_FORWARD_MESSAGE.
- Improved organization of tools by categorizing them under distinct sections for reading messages, managing drafts, and handling labels.
- Enhanced admin tools with new functionalities like GMAIL_BATCH_DELETE_MESSAGES and GMAIL_UNTRASH_THREAD, improving overall email management capabilities.

These changes provide a more comprehensive toolkit for interacting with Gmail, enhancing user experience and functionality.

* refactor(tool_scope, gmail): improve code formatting and organization

- Reformatted the `ADMIN` and `GMAIL_CURATED` constants for better readability by aligning the entries vertically.
- Enhanced the clarity of the `classify_unknown` function by improving the structure of the constants, making it easier to maintain and understand.
- Updated test assertions in `toolkit_from_slug` for consistency in formatting, ensuring clearer test outputs.

These changes improve the overall readability and maintainability of the codebase, aligning with ongoing refactoring efforts.

* feat(github): add GitHub toolkit and curated tools for enhanced integration

- Introduced a new GitHub provider module, including a curated catalog of GitHub actions tailored for common tasks such as repository management, issue tracking, and pull request handling.
- Implemented the `catalog_for_toolkit` function to allow fallback to a static curated list for toolkits without a native provider, ensuring consistent tool visibility and access.
- Updated the `evaluate_tool_visibility` function to prioritize curated tools from registered providers, enhancing the overall user experience and security by enforcing whitelist checks.

These changes expand the capabilities of the Composio toolkit, providing users with a comprehensive set of tools for interacting with GitHub, while maintaining a focus on user-defined permissions and visibility.

* refactor(tools): improve formatting and organization of curated tools

- Reformatted the `GITHUB_CURATED`, `NOTION_CURATED`, and other tool constants for better readability by aligning entries vertically.
- Enhanced the clarity of the code structure, making it easier to maintain and understand.
- Updated related documentation to reflect the changes in formatting and organization.

These changes improve the overall readability and maintainability of the codebase, aligning with ongoing refactoring efforts.

* feat(catalogs): introduce curated catalogs for various toolkits

- Added a new module `catalogs.rs` containing curated tool lists for Slack, Discord, Google Calendar, Google Drive, Google Docs, and more, enhancing the Composio toolkit's integration capabilities.
- Updated the `mod.rs` file to include the new `catalogs` module and modified the `catalog_for_toolkit` function to support these curated lists, allowing for better organization and access to toolkit actions.
- This addition improves the overall functionality and user experience by providing a comprehensive set of tools for interacting with popular platforms.

* feat(post-process): implement HTML to markdown conversion for Gmail responses

- Introduced a new `post_process` module to handle per-toolkit response modifications, specifically for converting HTML content to markdown format.
- Enhanced the `ComposioExecuteTool` to apply post-processing on successful responses, improving the clarity and usability of data returned from Gmail.
- Added tests to ensure the correct functionality of HTML detection and conversion, validating the integrity of the post-processing logic.

These changes enhance the user experience by streamlining the handling of HTML content in responses, making it more suitable for further processing and display.

* refactor(post_process): reorganize HTML detection constants for improved readability

- Moved HTML detection markers in the `looks_like_html` function to a more structured format, enhancing clarity and maintainability.
- This change aligns with ongoing efforts to improve code organization and readability within the post-processing module.

* refactor(catalogs): enhance formatting and organization of curated tool constants

- Reformatted the `SLACK_CURATED`, `DISCORD_CURATED`, and `GOOGLECALENDAR_CURATED` constants for improved readability by aligning entries vertically.
- This change enhances the clarity and maintainability of the code, aligning with ongoing refactoring efforts to improve code organization.

* feat(connect-modal): wire read/write/admin scope toggles to composio prefs

Adds the three scope toggles to the connected-state of the
ComposioConnectModal so users can gate which Composio actions the
agent may invoke per integration. Loads the stored pref via
`composio_get_user_scopes` once the modal lands in the connected
phase and persists changes through `composio_set_user_scopes` with
optimistic updates and rollback on error.

- Adds `getUserScopes` / `setUserScopes` to composioApi.
- Adds `ComposioUserScopePref` type mirror.
- Renders accessible role="switch" toggles with hint text per row.

* refactor(ComposioConnectModal): simplify SCOPE_ROWS definition

- Streamlined the definition of SCOPE_ROWS in ComposioConnectModal by removing unnecessary line breaks, enhancing code readability and maintainability.
- Updated the agent.toml configuration to include "composio_execute" in the tools list, expanding the capabilities of the integrations agent.

* fix(composio): apply curated whitelist + scope pref to integrations_agent prompt

The agent prompt for integrations_agent was rendering every action
returned by the backend's `composio_list_tools` for each connected
toolkit, bypassing the curation/scope filter that the meta-tool layer
applies. Concretely the GitHub integrations_agent prompt was showing
~500 actions including non-curated entries like
GITHUB_ADD_OR_UPDATE_TEAM_REPOSITORY_PERMISSIONS.

Adds `is_action_visible_with_pref(slug, pref)` — a sync helper that
mirrors the meta-tool layer's decision logic — and applies it in:
- `fetch_connected_integrations_uncached` (bulk session-cached path)
- `fetch_toolkit_actions` (per-toolkit spawn-time path)

One pref load per toolkit (not per action) keeps the cost minimal.

* refactor(fetch_connected_integrations): streamline action visibility filter

Simplified the action visibility filter in `fetch_connected_integrations_uncached` by consolidating the filter logic into a single line. This change enhances code readability while maintaining the existing functionality of applying user preferences to the displayed actions.

* fix(prompt): drop duplicate `### Available Tools` listing in text-mode preamble

Text-mode subagent prompts were rendering the tool catalog twice: once
in the prompt template's `## Tools` section (with the richer
`Call as: NAME[arg|arg]` signatures from `prompts::ToolsSection::build`)
and once in `### Available Tools` under `## Tool Use Protocol`
(`Parameters: name:type, ...` format).

For an integrations_agent toolkit spawn (~50 actions) this doubled the
tool listing bytes for no informational gain. Keep only the protocol
preamble (essential for text mode); the catalog stays in `## Tools`.

Removes `summarise_parameters` and `first_line_truncated` which were
the sole consumers, plus the now-unused `std::fmt::Write` import.

* review: address PR feedback (UTF-8 boundary, structured logs, doc, debug)

Real fixes:
- post_process: walk back to UTF-8 char boundary before truncating to
  4096 bytes; previous `&s[..4096]` could panic mid-codepoint. Adds
  regression test that places a 3-byte char straddling the cutoff.
- composioApi: move misplaced `execute` docstring back above `execute`
  (it had drifted above the new `getUserScopes`).
- schemas: include `get_user_scopes` / `set_user_scopes` in the
  `every_known_schema_key_resolves` test.

Diagnosability:
- schemas: structured `[composio:scopes]` debug/error logs at entry,
  exit, and every early-return in `handle_get_user_scopes` /
  `handle_set_user_scopes` (method + toolkit + pref fields). The
  memory-not-ready branch now logs an error before returning.
- composioApi + ConnectModal: grep-friendly `[composio][scopes]` debug
  logs around getUserScopes / setUserScopes RPC round-trips and the
  toggle handler (old → new state, persisted result, errors).
- user_scopes: load_or_default now logs the same normalized `key` that
  `load()` does, so traces correlate across both code paths.

Cleanups:
- tools: replace external `idx` + `Vec::retain` in
  `filter_list_tools_response` with a drain + zip + filter_map pattern.
- tool_scope: document the no-underscore-in-toolkit-name assumption of
  `toolkit_from_slug`, and call out the `microsoft_teams` alias.
- Cargo.toml: comment on the html2md vs htmd choice.

Pushback (no change):
- Reviewer asked to split the type-only import in ConnectModal into a
  separate `import type` line; ESLint's `no-duplicate-imports` rejects
  that, so the inline `type` form (functionally identical) stays.
2026-04-17 21:12:17 -07:00
Steven EnamakelandGitHub fff24a6c6f fix(prompts): v2 prompt pipeline, integrations_agent, and subagent session plumbing (#642)
* refactor(transcript): update session transcript paths and enhance directory structure

- Changed the source of truth path for session transcripts from `sessions/{DDMMYYYY}/{agent}_{index}.jsonl` to `session_raw/{DDMMYYYY}/{agent}_{index}.jsonl` to better reflect the organization of files.
- Updated the logic for creating and resolving transcript paths to accommodate the new directory structure, ensuring compatibility with legacy `.md` files.
- Improved documentation to clarify the changes in file organization and their implications for transcript management.

This refactor enhances the clarity and maintainability of session transcript handling by establishing a more logical file structure.

* refactor(prompts): update tool handling in prompt builders

- Replaced `Vec<String>` with `Vec<ToolSummary<'_>>` for available tools in multiple agent prompt builders, enhancing type safety and clarity.
- Introduced `render_tool_catalog` and `render_connected_integrations` functions to dynamically generate sections in prompts based on available tools and connected integrations.
- Updated the `build` function in various agent prompts to utilize the new rendering functions, ensuring that prompts accurately reflect the current context and available resources.

These changes improve the maintainability and functionality of the prompt generation process across different agents.

* refactor(prompts): streamline prompt builders for agent templates

- Updated the prompt builders for various agents to utilize the sibling `prompt.md` template directly, enhancing clarity and maintainability.
- Replaced `Vec<ToolSummary<'_>>` with `Vec<ToolSummary>` and `Vec<ConnectedIntegration>` for improved type safety in test cases.
- Adjusted the `build` function to ensure consistent formatting and handling of tool catalogs across different agents.

These changes simplify the prompt generation process and prepare the codebase for future enhancements.

* refactor(harness): unify tool filtering and prompt loading for debug consistency

- Exposed `filter_tool_indices` and `load_prompt_source` as `pub(crate)` to ensure that both the live runner and debug dump share the same filtering and loading logic, eliminating discrepancies.
- Enhanced documentation for both functions to clarify their purpose and usage, improving maintainability and understanding of the codebase.

These changes streamline the tool management process and enhance the reliability of debug outputs, ensuring consistency across different contexts.

* refactor(prompts): enhance prompt builders for agent templates

- Updated the prompt builders for various agents to return fully-assembled system prompts, incorporating section helpers from `crate::openhuman::context::prompt`.
- Replaced `Vec<ToolSummary<'_>>` with `Vec<ToolSummary>` and `Vec<ConnectedIntegration>` for improved type safety in test cases.
- Adjusted the `build` function to ensure consistent formatting and handling of user files, tools, and workspace sections across different agents.

These changes streamline the prompt generation process, improve maintainability, and prepare the codebase for future enhancements.

* refactor(prompts): enhance prompt context handling for dynamic sources

- Updated the prompt builders to support fully-assembled prompts from dynamic sources, allowing for more flexible prompt generation.
- Introduced `PromptTool` and `PromptContext` structures to replace `ToolSummary`, improving type safety and clarity in prompt construction.
- Refactored the handling of prompt sources in both the subagent runner and session builder to streamline the integration of dynamic prompts and legacy sources.

These changes improve the maintainability and functionality of the prompt generation process, ensuring accurate representation of available tools and context in agent interactions.

* refactor(prompts): improve prompt context and tool handling

- Enhanced the `PromptContext` structure to include additional fields for better context management, such as `skills`, `dispatcher_instructions`, and `tool_call_format`.
- Replaced `ToolSummary` with `PromptTool` for improved type safety and clarity in prompt generation.
- Updated the handling of dynamic prompt sources in both the subagent runner and debug dump, ensuring consistent integration and rendering of prompts.
- Introduced a mechanism to handle empty visible tool names, enhancing the robustness of prompt generation.

These changes streamline the prompt construction process and improve the overall maintainability of the codebase.

* refactor(prompts): reorganize prompt handling and introduce SystemPromptBuilder

- Moved prompt-related types and builders from `openhuman::context::prompt` to `openhuman::agent::prompts` for better modularity.
- Introduced `SystemPromptBuilder` to streamline the construction of system prompts, allowing for flexible section management.
- Updated module exports to maintain compatibility while enhancing the organization of prompt-related code.

These changes improve the clarity and maintainability of the prompt generation process, aligning it more closely with the agents that utilize these prompts.

* refactor(prompts): unify agent prompt handling and update CLI references

- Removed the "main" alias for the orchestrator in the prompt dumping process, treating it as just another registered agent.
- Updated the `debug-agent-prompts.sh` script to reflect this change, ensuring all agents are included uniformly.
- Revised documentation and error messages in `agent_cli.rs` to replace references to "main" with "orchestrator" for clarity.
- Enhanced the debug dump functionality to maintain consistency across agent prompts, improving overall maintainability and usability.

These changes streamline the prompt handling process and clarify the usage of agent identifiers in the CLI, aligning with the new architecture.

* refactor(prompts): remove CACHE_BOUNDARY references from agent prompts

- Eliminated the CACHE_BOUNDARY marker from various agent prompt files, streamlining the prompt generation process.
- Updated the build functions in multiple agents to ensure consistent handling of workspace rendering without the cache boundary.
- Refactored related prompt handling logic to enhance clarity and maintainability, aligning with the new architecture.

These changes simplify the prompt structure and improve the overall efficiency of prompt generation across agents.

* refactor(prompts): remove cache boundary references from tests and prompts

- Eliminated all instances of cache boundary references from the subagent runner and related tests, simplifying the prompt handling logic.
- Updated test assertions to reflect the removal of cache boundary checks, ensuring consistency across the testing framework.
- Refactored the session manager to streamline the system prompt assembly process without relying on cache boundaries.

These changes enhance the clarity and maintainability of the prompt generation process, aligning with the recent architectural updates.

* refactor(harness): clean up unused imports and streamline code

- Removed unnecessary imports from multiple files, including `RandomState`, `Hasher`, and `SerializeMap`, to enhance code clarity and maintainability.
- Simplified the structure of several modules by eliminating redundant use statements, ensuring a cleaner and more efficient codebase.

These changes contribute to a more organized and readable code structure, aligning with ongoing refactoring efforts.

* refactor(prompts): enhance agent prompt structures and integration handling

- Updated the `orchestrator`, `skills_agent`, and `welcome` prompts to streamline the rendering of connected integrations and delegation guides.
- Introduced dedicated functions for rendering integration information, ensuring clarity in the agent's voice and responsibilities.
- Removed redundant sections from the shared prompt builder, allowing each agent to manage its own prompt content more effectively.
- Improved test coverage for prompt generation, ensuring accurate representation of connected integrations and skills.

These changes enhance the maintainability and clarity of the prompt generation process, aligning with the recent architectural updates.

* refactor(session): update integration handling and clean up prompt parameters

- Revised documentation for `connected_integrations` in the `Agent` struct to clarify its role in the agent's prompt rendering.
- Updated the parameter name in `render_subagent_system_prompt_with_format` to `_connected_integrations` to indicate it is unused, enhancing code clarity.
- Cleaned up import statements in the context module for better organization and maintainability.

These changes improve the clarity of integration handling and streamline the code structure, aligning with ongoing refactoring efforts.

* refactor(agent_cli): simplify command options and improve documentation

- Removed the `--skill` option from the `dump-prompt` command, streamlining the command usage and focusing on essential parameters.
- Updated documentation to clarify the usage of the `dump-prompt` command and its parameters, enhancing user understanding.
- Cleaned up the `DumpFlags` structure by removing unused fields, contributing to a more maintainable codebase.

These changes improve the clarity and usability of the agent CLI, aligning with ongoing refactoring efforts.

* refactor(cli): streamline dotenv loading and clean up prompt rendering

- Introduced `load_dotenv_for_cli` to load environment variables for all CLI entrypoints, ensuring consistent configuration across commands.
- Updated documentation to clarify the purpose of the dotenv loading mechanism.
- Removed unnecessary blank lines in prompt rendering functions across multiple agents, enhancing code readability.

These changes improve the maintainability and clarity of the CLI and prompt handling, aligning with ongoing refactoring efforts.

* refactor(debug-agent-prompts): enhance environment loading and streamline workspace resolution

- Updated the script to load environment variables from a `.env` file, ensuring consistent configuration for prompt generation.
- Simplified workspace resolution by delegating to the binary's internal logic, improving reliability and reducing code duplication.
- Revised documentation to clarify the usage of command options and the impact of environment variables on prompt rendering.

These changes improve the maintainability and clarity of the debug agent prompts script, aligning with ongoing refactoring efforts.

* refactor(agent): remove category filter and simplify agent definitions

- Eliminated the `category_filter` from various agent definitions and related tests, streamlining the agent configuration.
- Updated the `run_list` function in `agent_cli.rs` to reflect the removal of category filtering, enhancing output clarity.
- Revised documentation and comments to remove references to the now-removed category filter, improving overall code maintainability.

These changes contribute to a cleaner and more efficient agent architecture, aligning with ongoing refactoring efforts.

* feat(agents): introduce integrations_agent and tools_agent for enhanced service handling

- Added the `integrations_agent` to manage service integrations via Composio, including a new TOML configuration and prompt structure.
- Introduced the `tools_agent` for general ad-hoc tasks using built-in OpenHuman tools, with its own configuration and prompt.
- Updated the loader to include both agents in the built-in agent list, increasing the total number of agents from 13 to 14.
- Revised orchestrator and welcome prompts to delegate integration tasks to the new `integrations_agent`, ensuring clarity in agent responsibilities.
- Enhanced tests to verify the registration and functionality of the new agents, improving overall test coverage.

These changes expand the capabilities of the agent architecture, allowing for more specialized handling of integrations and tool usage.

* refactor(agents): update references from skills_agent to integrations_agent

- Changed all instances of `skills_agent` to `integrations_agent` across various files, including prompts, CLI commands, and tool registrations.
- Updated documentation and comments to reflect the new agent name, ensuring clarity in agent responsibilities and usage.
- Revised debug scripts to align with the new prompt structure for the integrations agent.

These changes enhance consistency in the codebase and improve the clarity of agent interactions.

* refactor(agents): rename skills_agent to integrations_agent throughout the codebase

- Updated all instances of `skills_agent` to `integrations_agent` in various files, including tests, documentation, and comments.
- Ensured consistency in agent references to improve clarity in agent responsibilities and interactions.
- Revised related code structures to align with the new naming convention, enhancing overall maintainability.

These changes support the transition to the new agent architecture and improve code readability.

* refactor(prompts): implement dynamic prompt rendering for enhanced context handling

- Introduced `DynamicPromptSection` to allow prompts to be built dynamically using a function pointer, enabling real-time access to the `PromptContext`.
- Updated `SystemPromptBuilder` to support dynamic prompts, ensuring that late-arriving state like `connected_integrations` is accurately reflected in the rendered output.
- Revised the prompt handling logic in the agent builder to streamline the integration of dynamic prompts, improving overall flexibility and responsiveness.

These changes enhance the prompt generation process, aligning with the ongoing improvements in agent architecture and context management.

* refactor(prompts): refine delegation guide to display only connected integrations

- Updated the `render_delegation_guide` function to list only the toolkits that are actively connected, omitting unauthorized toolkits to prevent hallucinations during delegation.
- Revised related tests to ensure the prompt correctly reflects the current state of integrations, including scenarios where no integrations are connected.
- Introduced a new utility function to filter out welcome-only tools from non-welcome agents, enhancing the clarity and safety of tool visibility.

These changes improve the accuracy and focus of the delegation guide, aligning with the ongoing enhancements in agent prompt handling.

* refactor(planner): update tool usage and prompt guidelines for read-only operations

- Modified the `agent.toml` configuration to clarify that the planner operates in a read-only mode, specifying that it does not mutate the workspace or memory.
- Revised the prompt guidelines to reflect the read-only nature of the planner, emphasizing the need for explicit nodes for any required writes to be handled by downstream agents.

These changes enhance the clarity of the planner's operational constraints and improve the overall structure of the planning process.

* refactor(config): remove web search enable flag and update related configurations

- Eliminated the `OPENHUMAN_WEB_SEARCH_ENABLED` environment variable and associated logic, as web search is now always enabled by default.
- Updated the configuration schema to reflect the removal of the enable flag from `WebSearchConfig`.
- Adjusted tool registration to ensure web search is always available, simplifying the configuration process.

These changes streamline the web search functionality, ensuring it is consistently available across all sessions.

* refactor(config): update http_request flag to always enabled

- Changed the `http_request` configuration to always be enabled, removing the dependency on the `config.http_request.enabled` flag.
- This adjustment simplifies the configuration process and ensures consistent behavior across the application.

These changes contribute to a more streamlined configuration and enhance the overall reliability of the onboarding process.

* refactor(debug-agent-prompts): transition to dump-all command for agent prompts

- Replaced the previous method of listing agent IDs and dumping prompts with a new `dump-all` command that consolidates the functionality into a single call.
- Updated the script to handle output directory and workspace options more efficiently, leveraging Rust's `dump_all_agent_prompts` for processing.
- Enhanced the handling of the `integrations_agent` to generate separate dumps for each connected toolkit, improving the clarity and organization of output files.
- Revised related logging and summary generation to reflect the new structure, ensuring a more streamlined user experience.

These changes modernize the prompt dumping process, aligning it with the latest architectural improvements and enhancing usability.

* feat(composio): implement dynamic fetching of toolkit actions for integrations

- Added a new `fetch_toolkit_actions` function to retrieve the current action catalogue for a specified Composio toolkit, enhancing the responsiveness of the integrations agent.
- Updated the `subagent_runner` to utilize the fresh action list at spawn time, ensuring that the toolkit's actions reflect the latest backend state.
- Modified the `render_integrations_agent` function to refresh the action catalogue during prompt generation, improving the accuracy of the displayed tools.

These changes enhance the integration experience by providing up-to-date action information, aligning with the ongoing improvements in agent functionality.

* refactor(integrations-agent): update tool visibility and configuration handling

- Modified the `agent.toml` to replace `wildcard` with `named` tools, enhancing control over tool visibility for the integrations agent.
- Updated the `subagent_runner` to ensure that tool visibility aligns with the new TOML configuration, preventing unnecessary stripping of tools.
- Revised the `render_integrations_agent` function to respect the updated tool scope, improving the accuracy of the tool list generated for subagents.

These changes streamline the tool management process, ensuring that only explicitly defined tools are available during agent execution.

* feat(composio): add composio_list_connections tool for dynamic integration detection

- Introduced the `composio_list_connections` tool in the orchestrator's configuration, allowing the agent to detect newly-authorized Composio integrations mid-session.
- Enhanced the `ComposioListConnectionsTool` to filter and return only currently-connected integrations with ACTIVE or CONNECTED status, improving the accuracy of integration management.
- Updated the tool's description to clarify its functionality and usage context.

These changes enhance the agent's ability to manage integrations dynamically, aligning with ongoing improvements in the integration experience.

* fix(prompt): update delegation guide to reference Skills page

- Modified the `render_delegation_guide` function to change the reference from **Settings → Integrations** to the **Skills** page for connecting integrations. This update clarifies the user instructions for integration management.

* feat(session): introduce session key management for sub-agents

- Added `session_key` and `session_parent_prefix` fields to `ParentExecutionContext` to facilitate hierarchical transcript naming for sub-agents.
- Updated `persist_subagent_transcript` to generate transcript filenames based on the parent's session key, ensuring a flat file structure that reflects the parent-child relationship.
- Enhanced `AgentBuilder` and `Agent` to support the new session key management, allowing for better organization of session transcripts.
- Adjusted related functions to ensure proper handling of session keys during agent execution and transcript persistence.

These changes improve the clarity and organization of session transcripts, aligning with the ongoing enhancements in agent functionality.

* refactor(tests): update integrations agent tests for tool scope and transcript handling

- Renamed the `integrations_agent_is_wildcard` test to `integrations_agent_tool_scope_honours_toml` to better reflect its purpose of validating tool scope based on TOML configuration.
- Enhanced the test to assert that the `integrations_agent` correctly recognizes named tools instead of a wildcard.
- Removed the `integrations_agent_has_extra_tools_for_export` test as it is no longer relevant to the current tool management strategy.
- Improved the `latest_in_dir` function to clarify the handling of transcript naming schemes, ensuring proper differentiation between legacy and keyed formats.

These changes streamline the testing process and improve the accuracy of tool scope validation, aligning with recent updates in agent functionality.

* refactor(subagent_runner): improve transcript persistence and streamline inner loop

- Removed the `persist_subagent_transcript` function, transitioning transcript persistence to occur per-iteration within the `run_inner_loop`, enhancing reliability by ensuring transcripts are written immediately after each provider response.
- Updated the handling of session keys to maintain consistent naming for transcripts, reflecting the parent-child relationship in the file structure.
- Simplified the code by eliminating redundant post-loop transcript writes, aligning with recent changes in agent functionality and improving overall clarity in transcript management.

* refactor(agent_cli, subagent_runner, session): improve code readability and formatting

- Enhanced formatting in `agent_cli.rs` for better readability by adjusting the structure of string formatting.
- Streamlined conditional checks in `subagent_runner.rs` to improve clarity and maintainability.
- Simplified the handling of agent IDs in `builder.rs` to reduce line length and improve code flow.
- Updated test cases in `tests.rs` for better alignment and readability of expected values.
- Improved formatting in `debug_dump.rs` to enhance the clarity of toolkit action fetching and logging.

These changes collectively enhance the overall readability and maintainability of the codebase, aligning with ongoing refactoring efforts.

* fix(tests): add missing session_key fields to ParentExecutionContext stub

* fix: address PR review feedback

* fix(prompts-v2): round 2 PR review — dispatcher instructions, workspace-file preservation, cached-tool fallback, transcript persistence

- subagent_runner: populate PromptContext.dispatcher_instructions for Dynamic prompts (was empty string, dropping the ## Tool Use Protocol block in render_tools for PFormat/Json/Native sub-agents)
- subagent_runner: add post-tool persist_transcript after tool results are appended so a mid-round crash doesn't lose tool outputs
- prompts::sync_workspace_file: preserve user-edited workspace files — only overwrite when the file doesn't exist OR its current hash matches the stored builtin hash
- context::debug_dump: mirror runner's cached-tool fallback — keep cached action catalogue on empty/error from fetch_toolkit_actions instead of blanking it
- core::agent_cli: add entry/exit debug logs around dump_all / dump_prompt calls and a trace log around each prompt file write; update module banner to note --toolkit is required when --agent is integrations_agent
2026-04-17 19:47:31 -07:00
8d4934e634 feat(agent): progressive-disclosure handoff + token-based summarizer threshold (#574) (#586)
* feat(agent): summarizer sub-agent compresses oversized tool results

Issue #574.

Before this change, tool results larger than a few KB landed verbatim
in orchestrator history, burning context budget on raw JSON/HTML/file
dumps. The only guardrail was tool_result_budget_bytes which
hard-truncated mid-payload, dropping everything past the cut.

This PR introduces a dedicated `summarizer` sub-agent
(model.hint = "summarization") that the runtime automatically
dispatches whenever a tool result exceeds
summarizer_payload_threshold_bytes (default 100 KB). The summarizer
compresses the payload per an extraction contract that preserves
identifiers and key facts, and the compressed summary replaces the
raw payload before it enters agent history. Payloads above
summarizer_max_payload_bytes (default 5 MB) skip summarization and
fall through to the existing truncation path — paying for an LLM
call on a 5 MB blob is counterproductive.

Scoped to the orchestrator session only. Welcome, skills_agent,
researcher, planner, and every other typed sub-agent get None and
their tool results are untouched. Gated in
build_session_agent_inner by checking agent_id == "orchestrator".

Instrumented at both tool-loop paths:
  - run_tool_call_loop in tool_loop.rs (event-bus/channels path)
  - Agent::execute_tool_call in session/turn.rs (web channel path
    via run_single)

Both paths call into the same `PayloadSummarizer::maybe_summarize`
trait so the threshold check, circuit breaker (3 consecutive
failures disables for the session), and sub-agent dispatch policy
live in exactly one place (agent/harness/payload_summarizer.rs).

The summarizer sub-agent is runtime-dispatched only — it is NOT
exposed as a delegation tool to the orchestrator's LLM. It is listed
in the orchestrator's `subagents = [...]` for explicit registration,
but `collect_orchestrator_tools` filters out the `summarizer` id and
never synthesises a `delegate_summarizer` tool.

Files:
  * NEW: src/openhuman/agent/agents/summarizer/{agent.toml,prompt.md}
  * NEW: src/openhuman/agent/harness/payload_summarizer.rs (trait +
    SubagentPayloadSummarizer impl + circuit breaker + tests)
  * src/openhuman/agent/agents/mod.rs — register summarizer in
    BUILTINS
  * src/openhuman/agent/agents/orchestrator/agent.toml — list
    summarizer in subagents (runtime-only, no LLM delegation tool)
  * src/openhuman/agent/harness/mod.rs — declare module
  * src/openhuman/agent/harness/builtin_definitions.rs — expect
    summarizer in `expected_builtin_ids_are_present`
  * src/openhuman/agent/harness/tool_loop.rs — thread Option<&dyn
    PayloadSummarizer> into run_tool_call_loop + agent_turn,
    intercept at the tool-execution success site, plus a new
    integration test using a MockSummarizer
  * src/openhuman/agent/harness/tests.rs — thread None through the
    existing run_tool_call_loop callsites
  * src/openhuman/agent/harness/session/turn.rs — same interception
    in Agent::execute_tool_call
  * src/openhuman/agent/harness/session/types.rs — payload_summarizer
    field on Agent and AgentBuilder
  * src/openhuman/agent/harness/session/builder.rs —
    .payload_summarizer() setter + orchestrator-only construction in
    build_session_agent_inner
  * src/openhuman/agent/bus.rs — pass None for the bus path
  * src/openhuman/config/schema/context.rs —
    summarizer_payload_threshold_bytes + summarizer_max_payload_bytes
    on ContextConfig
  * src/openhuman/tools/orchestrator_tools.rs — filter out the
    summarizer id from delegation-tool synthesis

Tests: unit tests in payload_summarizer pin the pass-through rules
(below threshold, above max cap, breaker tripped) and the prompt
construction. Integration test in tool_loop::tests uses a
MockSummarizer to verify the interception wires through end-to-end.

Closes tinyhumansai/openhuman#574.

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

* feat(tools): csv_export tool + skills_agent oversized output handling

When a Composio tool returns a large payload inside skills_agent,
the agent now has two prompt-driven paths:

  Path A — user wants an answer derived from the data:
    skills_agent extracts the answer in its next iteration
    and returns a targeted response (no file I/O needed).

  Path B — user wants the actual raw dataset:
    skills_agent calls csv_export (tabular data) or file_write
    (non-tabular) to persist the output to workspace/exports/,
    then returns a summary + file path.

Changes:
  * NEW: src/openhuman/tools/impl/filesystem/csv_export.rs
    — CsvExportTool: parses JSON array, formats as CSV, writes
    to workspace/exports/{filename}. Handles missing keys
    (empty cells), nested values (JSON-serialised), and optional
    column ordering. Sandboxed via SecurityPolicy.
  * src/openhuman/tools/impl/filesystem/mod.rs — wire module
  * src/openhuman/tools/ops.rs — register CsvExportTool
  * src/openhuman/agent/harness/definition.rs — new extra_tools
    field on AgentDefinition, allowing named system tools to
    bypass category_filter
  * src/openhuman/agent/harness/subagent_runner.rs — inject
    extra_tools into allowed_indices after category filtering
  * src/openhuman/agent/agents/skills_agent/agent.toml
    — add file_write + csv_export via extra_tools
  * src/openhuman/agent/agents/skills_agent/prompt.md
    — new "Handling Oversized Tool Results" section with
    Path A (extract answer) vs Path B (export file) decision
    tree, intent-detection heuristics, and examples

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

* feat(agent): progressive-disclosure handoff for skills_agent + token-based summarizer threshold (#574)

Two layered fixes for #574 plus supporting changes.

## Summarizer thresholds in tokens, not bytes

Renamed `ContextConfig::summarizer_payload_threshold_bytes` ->
`summarizer_payload_threshold_tokens` (default 500 000, was 100 KB in
bytes) and `summarizer_max_payload_bytes` -> `summarizer_max_payload_tokens`
(default 2 000 000). Token count is estimated at ~4 chars/token via a
local helper that mirrors `tree_summarizer::types::estimate_tokens`.
Serde aliases on both fields keep existing config.toml files parsing.
`payload_summarizer.rs` now compares token estimates for the threshold
and cap, with both `tokens=` and `bytes=` surfaced in log events.

## Progressive-disclosure handoff for skills_agent

`skills_agent` typed subagents (when spawned with a Composio toolkit)
now receive a per-spawn `ResultHandoffCache` and a new built-in
`extract_from_result` tool. When a per-action tool returns more than
`HANDOFF_OVERSIZE_THRESHOLD_TOKENS` (20 000 tokens), the raw payload
is stashed in the cache and a compact placeholder replaces it in
history:

    [oversized tool output: 187432 bytes - stashed as result_id="res_1"]
    Preview (first 1500 chars):
    ...
    If the preview does not answer your task, call:
    extract_from_result(result_id="res_1", query="<specific question>")

The subagent can answer from the preview, call `extract_from_result`
with a targeted query, or re-call the original tool with tighter
filters. `extract_from_result` dispatches the `summarizer` subagent
against the cached payload with the query as the extraction contract.

For payloads over `SUMMARIZER_CHUNK_CHAR_BUDGET` (60 000 chars) the
extractor runs a chunked map-reduce:
- split at blank-line / newline boundaries
- map each chunk to a per-chunk partial via bounded concurrency
  (`buffer_unordered(3)`) to avoid storming the provider gateway
- reduce partials in one more summarizer turn, or fall back to the
  concatenation if the reduce stage fails

Falls back gracefully when the summarizer definition is missing from
the registry (placeholder + preview still work; just no extractor).

## Text-mode dispatcher for skills_agent + tighter tool filter

Provider-side grammar decoders (Fireworks etc.) cap tool-schema rules
at 65 535 (uint16_t). Large Composio toolkits - Gmail, Notion, GitHub,
Salesforce, HubSpot, Google Workspace - ship per-action JSON schemas
dense enough that even a handful of them exceed the cap.

- `subagent_runner::run_inner_loop` now detects `skills_agent` and
  omits `tools: [...]` from the API request, instead appending an
  `<tool_call>{...}</tool_call>` XML protocol block (with compact
  per-tool parameter summaries) into the system prompt. Tool calls
  are parsed out of the model's free-form response via the shared
  `parse_tool_calls` helper. Mirrors XmlToolDispatcher behaviour.
- `top_k_for_toolkit()` picks 12 for HEAVY_SCHEMA_TOOLKITS (the six
  listed above plus googledocs, googlesheets, microsoftteams) vs the
  default 25 for lighter toolkits.

## Skills_agent agent.toml / prompt changes

- Dropped `csv_export` from `skills_agent.extra_tools` (kept
  `file_write`) - the export tool was triggering superfluous file
  writes after extraction had already answered the query.
- `builtin_definitions::skills_agent_has_extra_tools_for_export` test
  inverted to assert `csv_export` is NOT present.
- `prompt.md` re-aligned: Path B now references only `file_write`.

## Wiring

- `session/builder.rs`: threshold rename wiring (`_tokens`).
- `subagent_runner.rs::run_inner_loop`: new `handoff_cache` parameter
  (threaded through both call sites; fork-mode passes `None`).

## Tests

All green:
- `payload_summarizer::tests`: 6/6 (thresholds in tokens)
- `tool_loop::tests`: 10/10 (MockSummarizer integration)
- `subagent_runner::tests`: 19/19
- `builtin_definitions::tests`: 5/5 (csv_export removal)
- `csv_export::tests`: 7/7 (implementation kept, wiring removed)

Closes #574.

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

* perf(agent): drop reduce LLM call and pre-clean tool outputs in chunked extraction

Two optimisations to the oversize-handoff / extract_from_result pipeline:

1. Concat chunk summaries in order; drop reduce stage.
   The reduce-stage summarizer call was the slowest single turn of the
   pipeline and a single point of failure when the upstream provider
   hung — a hung reduce burned minutes before timing out. Map summaries
   are now sorted by original chunk index and concatenated with a
   plain-separator join. For listing/extraction queries this is
   equivalent; for top-N / global-ordering queries the caller can
   post-process. Observed: 8m24s → ~70s on a Notion SEARCH run.

2. Generic cleaner before the oversize check.
   Strips <script>/<style>/<svg> blocks, HTML comments, data:...;base64,...
   inline URIs, remaining HTML tags, and collapses whitespace. Applied
   on every tool output so results that are mostly markup drop below
   the 20k-token threshold and skip the extract pipeline entirely; the
   ones that remain oversized get chunked on clean bytes. Debug log
   emits before/after bytes + saved_pct per call.

No changes to the extract_from_result tool surface or placeholder format.

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

* fix(chat): rearm 2-min silence timer on inference progress events

The client-side safety timer in Conversations.tsx was a flat 120s
deadline from the send, not a silence window. Long agent turns
(chained tool calls, chunked extraction fan-outs, subagent spawns)
would trip the timeout even though the backend was actively making
progress — showing "No response from the assistant after 2 minutes"
while the server was still working.

The effect now watches inferenceStatusByThread and rearms the timer
on every status change for the sending thread. Any tool_call /
tool_result / iteration_start / subagent_{spawned,done} event resets
the 120s window. When status is cleared (chat_done / chat_error),
the timer is dropped. A truly silent server still fails after 120s
as before.

Tracks the sending thread id in a ref (sendingThreadIdRef) so
switching threads mid-turn doesn't move the timer's reference point.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 14:09:00 -07:00
c7487324cc feat(channels): dynamic filler messages during long agent turns (#600) (#636)
* refactor(channels): add extract_message_id helper for varied response shapes

Backend send_channel_message responses use at least three shapes:
{"id":"..."}, {"data":{"id":"..."}}, and {"messageId":1456,"success":true}.
The last one returns the id as a JSON number, so the prior inline
as_str()-only extraction silently dropped it. Consolidate the extraction
into a single helper that handles str/i64/u64 candidates and routes both
streaming-edit and thinking-message send paths through it. Fixes the
silent id loss that left thinking bubbles undeletable (#600).

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

* fix(channels): latch thinking edits off when first POST yields no id

When the initial thinking POST returned 200 but carried no message id
(either because the response shape was unexpected or the send itself
failed before C1), every subsequent thinking_dirty tick re-entered the
"send new message" branch. The user then saw one standalone italic
bubble per accumulated snippet instead of a single evolving one. Add a
thinking_edit_disabled latch that is set in both failure paths and
guard the edit_timer branch on it so we post at most one thinking
bubble per turn when the id is unrecoverable (#600).

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

* style(channels): label thinking bubble with explicit "Thinking:" header

A bare italicized snippet under a 💭 emoji reads ambiguously in chat —
it could be a user quote, a system note, or assistant output. Prefix
the italic body with an explicit "Thinking:" line so the ephemeral
bubble is unmistakably the LLM's reasoning stream and visually distinct
from both filler messages and the final reply (#600).

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

* fix(channels): reply-first finalize with orphan draft recovery

Two problems in the old finalize path. (1) The thinking bubble was
deleted before the reply was sent, leaving the chat momentarily empty
between the two round-trips. (2) When the final edit failed, the half-
streamed draft was left in place and the user never received the
canonical response — a silent data-loss hole during edit-endpoint
flakiness. Wrap the three delivery paths in a 'send: labeled block so
they share a single cleanup tail, delete the orphan draft and send a
fresh atomic reply on edit failure, and move the thinking-bubble
deletion to after the reply is on screen so the chat never blinks
empty (#600).

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

* feat(channels): ephemeral filler messages every 13s with dynamic snippets

Long agent turns (30–90 s) leave the chat static once the thinking
stream goes quiet and progressive edits stop, which reads as a frozen
bot. Add a third timer branch in the inbound loop that posts a short
"still working" message every FILLER_INTERVAL (13 s, tuned to stay
inside Telegram's ~1 msg/sec chat cap with headroom).

Each filler prefers a tail slice of the live thinking_accumulator
(last MAX_FILLER_CHARS = 200 Unicode scalars, trimmed at a word
boundary so it reads cleanly) so the user sees the agent's actual
reasoning instead of canned text. When the accumulator hasn't advanced
since the last filler we fall through to a rotating STATIC_FILLERS
pool ("💭 Still working on it…", etc.) so the chat still moves. All
filler ids are tracked in StreamingState.filler_message_ids and
deleted in finalize_channel_reply alongside the thinking bubble, so
the user ends each turn seeing only the canonical reply. A
filler_disabled latch stops hammering endpoints that reject filler
sends twice in a row.

Verified end-to-end on Telegram with both long (74 s, 5 fillers) and
short (32 s, 2 fillers) turns — all ephemeral messages cleaned up
cleanly after the final reply landed (#600).

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 13:03:49 -07:00
da5fe9260b fix(local_ai): hard-override to disabled until explicit opt-in (#573) (#637)
* refactor(local_ai): default to opt-in on all devices (#573)

Local AI now defaults to disabled whenever the user has not explicitly
picked a tier, regardless of device RAM. The onboarding flow and
Settings panel remain the only ways to turn it on. Previously the
bootstrap only disabled local AI on <8 GB devices and auto-applied a
recommended preset on larger hosts; this flip completes the MVP goal
of cloud-first defaults with a single, opt-in local model.

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

* style(local_ai): apply cargo fmt to bootstrap tests (#573)

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

* feat(onboarding): present cloud AI as default on sufficient-RAM path (#573)

Flips the LocalAIStep sufficient-RAM screen so the primary button is
"Continue with Cloud" and local AI appears as an explicit opt-in
("Use local AI instead"). This aligns onboarding with the new opt-in
bootstrap: every device now starts on cloud unless the user chooses
local AI. The low-RAM cloud-fallback screen is unchanged.

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

* test(onboarding): cover opt-in local AI semantics in LocalAIStep (#573)

Updates the sufficient-RAM path tests to match the cloud-primary UI:
the default "Continue with Cloud" click advances without triggering
local AI bootstrap, and the secondary "Use local AI instead" opt-in
still starts the recommended-preset bootstrap and propagates errors.
Low-RAM cloud-fallback tests are unchanged.

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

* feat(local_ai): add opt_in_confirmed marker to local AI config (#573)

Bootstrap will hard-override `enabled=false` unless this marker is true,
ensuring existing installs with a stale `selected_tier` from the pre-MVP
default-on era fall back to cloud until the user explicitly re-opts in.

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

* fix(local_ai): hard-override local AI to disabled until explicit opt-in (#573)

Every bootstrap path now returns `enabled=false` unless `opt_in_confirmed`
is true, regardless of device RAM or `selected_tier`. This closes the
regression where upgrading users with a persisted `selected_tier` bypassed
the onboarding opt-in and started local AI without consent.

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

* refactor(local_ai): set opt_in_confirmed on apply_preset and surface MVP tier only (#573)

`apply_preset` is the single source of truth for the opt-in marker: any
non-disabled tier flips it true, `disabled` clears it. The preset RPC now
returns `mvp_presets()` so the Settings UI exposes only the allowlisted
`ram_2_4gb` tier, matching the MVP scope already enforced on the
onboarding path.

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

* revert(local_ai): keep full preset catalog in presets RPC (#573)

Revert the `mvp_presets()` swap in `handle_local_ai_presets`. PR #588
already renders all 5 tier cards in Settings with non-MVP tiers shown
as "Coming soon" / non-selectable, and that roadmap visibility is the
intended UX. Returning only the MVP tier from the RPC hid the other 4
cards entirely and broke that signal.

The opt-in gate still holds: `apply_preset` remains the single writer
of `opt_in_confirmed`, the RPC guard continues to reject non-MVP
apply_preset calls, and the bootstrap hard-override still clamps
stale configs. This commit only rolls back the UI catalog surface.

Fixes failing `json_rpc_local_ai_device_profile_and_presets` integration
test which expects 5 presets.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 13:03:34 -07:00
Steven EnamakelandGitHub 156944478d feat(cache): wire-level prompt/response dumps + JSONL into session_raw/ (#640)
* feat(transcript): enhance session transcript persistence with per-turn usage tracking

- Updated the `write_transcript` function to accept an optional `last_assistant_turn_usage` parameter, allowing for the inclusion of per-message token and cost figures in the JSONL output.
- Modified the `persist_session_transcript` method to capture and pass the last turn's usage data to the transcript writer, ensuring accurate tracking of resource usage.
- Improved documentation to clarify the new functionality and its impact on transcript persistence.

This change enhances the fidelity of session transcripts by associating usage metrics with the last assistant message, improving overall transparency in resource consumption.

* refactor(transcript): improve transcript reading logic by routing based on file extension

- Renamed `read_transcript` function to clarify its purpose and updated its logic to first check the file extension.
- Added handling for legacy `.md` files, ensuring they are processed with the appropriate parser.
- Enhanced fallback mechanism to check for the existence of a `.md` sibling file if the primary JSONL file is not found.

This change improves the robustness of transcript reading by accommodating legacy formats while maintaining compatibility with new JSONL files.

* feat(transcript): mirror final assistant reply in session transcript

- Added functionality to capture the final assistant reply in the transcript snapshot, ensuring that both the prompt and response are persisted in the JSONL output.
- This enhancement improves the completeness of session transcripts by including the assistant's final message alongside the user prompts.

This change enhances the fidelity of session transcripts, providing a more comprehensive record of interactions.

* feat(cache): wire-level prompt/response dumps + split JSONL into session_raw/

Two related changes for KV-cache debugging and on-disk clarity:

1. Wire-level dump (`providers/compatible.rs`)
   When OPENHUMAN_PROMPT_DUMP_DIR is set, every chat completion writes
   the outgoing NativeChatRequest body and the aggregated ApiChatResponse
   to paired timestamped JSON files. Covers both the non-streaming and
   SSE streaming paths. Unset → zero overhead, no behavior change.

   Why: the prompt-level transcript captures what the dispatcher
   assembled, but the actual wire bytes (including
   prompt_tokens_details.cached_tokens and the openhuman meta block)
   only surface at the HTTP layer. Pairing request + response per turn
   lets us diff consecutive turns to confirm cache-hit accounting and
   diagnose cache-miss causes.

2. JSONL source-of-truth moves to session_raw/ (`agent/harness/session/transcript.rs`)
   JSONL transcript files now live under workspace/session_raw/DDMMYYYY/
   instead of workspace/sessions/. The human-readable .md companion
   stays in workspace/sessions/DDMMYYYY/ so the structured source-of-
   truth and the readable debug view are cleanly separated on disk.

   - resolve_new_transcript_path returns session_raw/ paths and
     advances the index past both session_raw/*.jsonl and legacy
     sessions/*.md so indices stay unique across the migration.
   - find_latest_transcript prefers session_raw/ and falls back to the
     legacy sessions/ dir for pre-migration .md files (one-release compat).
   - md_companion_path derives the sessions/ md path from the
     session_raw/ jsonl path via path-component rewrite, with a
     sibling-file fallback for tests that use flat tempdirs.

* fix(cache,transcript): address PR review findings

- transcript: .md companion write is now best-effort — failures are
  logged with log::warn! and Ok(()) is returned. The JSONL above is
  the source of truth; a readable-log hiccup must not take down state
  persistence.
- compatible: replace peek_dump_seq() (non-atomic) with
  reserve_dump_seq() that fetch_adds PROMPT_DUMP_SEQ in one step and
  returns the reserved value. dump_prompt_if_enabled now takes seq as
  a parameter, so request/response files cannot be misaligned under
  concurrent requests.
- turn: explicitly clear last_turn_usage when resp.usage is None so
  the final assistant message can't inherit a prior iteration's stale
  numbers during a tool-using turn.
- transcript (nit): last-assistant lookup uses rposition; the emit
  loop pattern-matches (Option, Option) together so there's no
  separate unwrap.
- transcript (nit): JSONL meta parsing is positional — first non-empty
  line must be _meta or we error out. Replaces the substring-based
  heuristic that could false-positive on message content.
- compatible (nit): lower prompt/response dump success logs from info
  to debug — they fire on every turn when the env var is set.
2026-04-17 13:02:50 -07:00
YellowSnnowmannandGitHub 0c04314004 Refactor: rust modules (#633) 2026-04-17 09:08:53 -07:00
Mega MindandGitHub 43d44acc1a fix: wire proactive welcome into conversations (#635) 2026-04-17 09:07:07 -07:00
6733720df9 fix(channels): delete thinking messages after final response (#600) (#612)
* feat(channels): add send_channel_delete to REST client (#600)

Add DELETE method to BackendOAuthClient for removing channel messages.
Used to clean up ephemeral thinking indicators after final response delivery.

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

* fix(channels): show thinking ephemerally and delete on final response (#600)

Thinking deltas are accumulated and sent as a temporary "💭" message
on the channel. When the final agent response is ready the thinking
message is deleted via the new DELETE endpoint so the user only sees
the clean reply. This replaces the previous behaviour where thinking
messages persisted permanently in the Telegram chat.

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

* style(channels): apply cargo fmt to bus.rs (#600)

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 23:04:00 -07:00
f098cfda40 test(coverage): batches 13–18 — Rust unit tests for 40+ modules (#530) (#618)
* test(coverage): batch 13–14 — core/all registry, proxy_config tool (#530)

Add 36 new tests:
- core/all: registry integrity (no duplicates, schema-controller parity),
  rpc_method_name, namespace_description, validate_params, schema lookup
- proxy_config: parse_scope, parse_string_list (CSV, array, errors),
  parse_optional_string_update, env_snapshot, proxy_json, security gates

All 4096 tests pass.

* test(coverage): batch 15 — memory/store/unified/helpers pure functions (#530)

Add 31 new tests for helpers module:
- vec_to_bytes/bytes_to_vec roundtrip, cosine_similarity (identical,
  orthogonal, mismatched, zero), collapse_whitespace, normalize_search_text,
  tokenize_search_terms, normalize_graph_entity/predicate, json_string_array,
  merge_unique_string_arrays, json_i64 (int/float/missing/string),
  recency_score (current/old/future), chunk_document_content

All 4127 tests pass.

* test(coverage): batch 16 — terminal, supervision, security detect, trigger history (#530)

Add 27 new tests across 4 modules:
- accessibility/terminal: is_text_role, is_terminal_app, looks_like_terminal_buffer,
  extract_terminal_input_context, noise line detection
- channels/runtime/supervision: compute_max_in_flight_messages clamping
- security/detect: all sandbox backend fallback paths (landlock, firejail,
  bubblewrap, docker on non-linux), disabled-via-enabled-false
- composio/trigger_history: list_recent with limit, empty store, field validation

All 4154 tests pass.

* test(coverage): batch 17 — learning, update, encryption, doctor, service schemas + skills/types (#530)

Add 49 new tests across 6 modules:
- learning/schemas: catalog, schema validation, unknown fallback
- update/schemas: check/apply schemas, optional staging_dir, controller parity
- encryption/schemas: encrypt/decrypt schemas, read_required helper
- doctor/schemas: report/models schemas, read_optional helper (none/null/value/error)
- service/schemas: 8 lifecycle functions, restart optional params, daemon_host
- skills/types: ToolResult success/error/json/mixed, serde roundtrip, ToolContent

All 4203 tests pass.

* test(coverage): batch 18 — accessibility/keys key-state probes (#530)

Add tests for is_tab_key_down/is_escape_key_down (platform-safe),
macOS-specific constant validation.

All 4216 tests pass.

* test(coverage): batch 19 — service/restart, memory/store/factories (#530)

Add 7 new tests:
- service/restart: default source/reason, whitespace trimming, empty-string
  defaults, RestartStatus serde, startup delay noop
- memory/store/factories: effective_memory_backend_name, migration-disabled error

All 4223 tests pass.

* test(coverage): batch 20 — tree_summarizer schemas, subconscious schemas (#530)

Add 38 new tests across 2 modules:
- tree_summarizer/schemas: catalog parity, all 5 functions, param helpers
  (read_required, read_optional, read_optional_timestamp), type_name,
  namespace_input
- subconscious/schemas: catalog parity, all 10 functions, required input
  validation, field/field_req/field_opt helpers

All 4261 tests pass.

---------

Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
2026-04-16 20:42:50 -07:00
Mega MindandGitHub ec309c8f90 feat(onboarding): complete conversational onboarding stack and UX audit (#619)
* fix(onboarding): guard complete_onboarding against premature flag flip (#591)

- Remove premature `chat_onboarding_completed = true` flip from
  `set_onboarding_completed` in config/ops.rs. The flag is now
  exclusively owned by the welcome agent via `complete_onboarding`.

- Strip auto-finalize side-effect from `check_status` action. It is
  now pure read-only and returns `onboarding_status`, `exchange_count`,
  and `ready_to_complete` in the snapshot instead of `finalize_action`.

- Add engagement guard to `complete` action: rejects with a descriptive
  error unless exchange_count >= 3 OR ≥1 Composio integration connected.
  Auth check also enforced before any write.

- Add process-global `WELCOME_EXCHANGE_COUNT` (AtomicU32) with
  `increment_welcome_exchange_count()` / `get_welcome_exchange_count()`.
  Dispatch layer calls `increment_welcome_exchange_count()` each time a
  user message routes to the welcome agent.

- Update `welcome_proactive.rs` snapshot call to new signature and
  prompt copy to reflect `onboarding_status: "pending"` / no pre-flip.

- Add 7 pure-logic unit tests for `engagement_criteria_met`, plus tests
  for the new exchange counter and updated snapshot shape.

Fixes #591

* style: cargo fmt for complete_onboarding.rs

* feat(onboarding): inject CONNECTION_STATE block into welcome-agent turns (#593)

Add `build_connection_state_block()` to dispatch.rs: fetches current
Composio integration status (with a 3s timeout for graceful degradation)
and formats it as a `[CONNECTION_STATE]...[/CONNECTION_STATE]` block.

After `resolve_target_agent` selects the welcome agent, the block is
appended to the last user message in history so the agent always has
up-to-date connection state without spending a tool call. This captures
OAuth completions that happened mid-conversation (user clicked auth link
in chat, authenticated in browser, came back).

Scoped strictly to welcome-agent turns (chat_onboarding_completed=false).
Orchestrator turns are unaffected.

Fixes #593
Part of #599

* style: cargo fmt for dispatch.rs

* feat(welcome): parallel template messages with LLM inference for faster perceived welcome (#592)

Show two template messages immediately while the LLM runs in the
background, cutting the perceived wait from ~15s to ~0s:

- Template 1 (t≈0ms): time-of-day greeting that names any connected
  channels, built from the status snapshot without extra I/O
- Template 2 (t=4s): "Getting everything ready for you..." loading
  indicator, published via tokio::join! alongside the LLM future
- LLM response: published when inference completes, opened directly
  with personalised setup content (no duplicate greeting)

`run_proactive_welcome` now fires three `ProactiveMessageRequested`
events rather than one. The two template helpers (`time_of_day_greeting`,
`build_template_greeting`) are pure functions covered by 6 new unit
tests. `prompt.md` gains a proactive-invocation section explaining that
greeting templates are pre-delivered and the agent must skip them.

* feat(welcome): add composio_authorize tool and inline auth link guidance (#594)

Wire `composio_authorize` into the welcome agent's tool allowlist and
teach the prompt how to offer OAuth links directly in chat:

- agent.toml: add "composio_authorize" to the named tools list so the
  welcome agent can call it during multi-turn conversations
- prompt.md: new "Offering inline auth links" section covering the
  consent-first flow (offer → user agrees → call tool → markdown link →
  confirm success via [CONNECTION_STATE] block on next turn)
- prompt.md: toolkit slug reference table for the common services
- prompt.md: auth link rules (no speculative calls, no bare URLs,
  one service at a time, await CONNECTION_STATE before confirming)
- prompt.md: two new "What NOT to do" bullets for composio_authorize
  misuse patterns

* feat(welcome): conversational onboarding prompt for issue #595

- Rewrite prompt.md: multi-turn flow, Gmail-first, no silent first turn
- Align with check_status/complete and ready_to_complete semantics
- Update proactive injection copy to match new tool wording

Made-with: Cursor

* feat(onboarding): add completion readiness reason

Made-with: Cursor

* docs(ux): add onboarding and welcome audit for #563

Made-with: Cursor

* docs(welcome): clarify OAuth link behavior in onboarding prompt

Updated the onboarding prompt to explicitly state that clicking the Gmail connection link opens the user's default browser for authentication. Added guidance to return to the chat after completing the authorization process.

* fix(onboarding): resolve CI test and proactive greeting review

Made-with: Cursor
2026-04-16 14:14:24 -07:00
Steven EnamakelandGitHub 7db71408bd feat(local_ai): default to cloud fallback on <8GB RAM devices (#589)
* Enhance draft message handling in streaming edits

- Introduced a new `draft_sent` field in the `StreamingState` struct to track when a draft message has been posted, decoupling the existence of a draft from the ability to edit it.
- Updated the `flush_streaming_edit` function to set `draft_sent` upon posting a draft, ensuring that duplicate messages are not sent if the backend fails to return an ID.
- Modified the `finalize_channel_reply` function to prevent sending a fresh message if a draft was already posted without an ID, improving user experience by avoiding duplicate bubbles.

These changes enhance the robustness of message handling during streaming edits, ensuring a smoother interaction for users.

* feat(onboarding): enhance LocalAIStep for low-RAM device handling

- Added logic to determine if the device is below the RAM threshold, defaulting to a cloud AI fallback if necessary.
- Introduced a new UI for low-RAM devices, informing users about the cloud mode and providing options to continue with cloud AI or force-enable local AI.
- Updated the `ensureRecommendedLocalAiPresetIfNeeded` function to return a `recommend_disabled` flag based on device RAM.
- Enhanced tests to cover new cloud fallback UI and local AI consent handling.

These changes improve the onboarding experience by providing clearer options based on device capabilities.

* style: apply prettier formatting to LocalAIStep

* feat(local_ai): unlock model selection + add Disabled/cloud fallback option

- Remove MVP ceiling that clamped selection to the 2-4 GB tier, in bootstrap
  and in the apply_preset RPC — users can now pick any tier.
- Add special "disabled" tier string to apply_preset that toggles
  config.local_ai.enabled = false so the app uses the cloud summarizer.
- Presets RPC now returns local_ai_enabled so the UI can render the
  currently-active state correctly.
- Rewrite DeviceCapabilitySection: tiers are now clickable buttons that
  call apply_preset; adds a "Disabled — Cloud fallback" card at the top
  marked "Recommended" on low-RAM devices and "Active" when enabled=false.
- Remove the MVP message copy from the model tier panel.
- Remove unread-count badge from the bottom tab bar.

* refactor(onboarding): streamline LocalAIStep and update local AI preset handling

- Removed the `ensureRecommendedLocalAiPresetIfNeeded` function call from `LocalAIStep`, replacing it with `openhumanLocalAiPresets` to directly fetch preset information.
- Updated the logic in `ensureRecommendedLocalAiPresetIfNeeded` to ensure the recommended tier is applied correctly based on user consent, improving clarity in the local AI setup process.
- Enhanced tests to mock the new `openhumanLocalAiPresets` function, ensuring coverage for low-RAM device scenarios and local AI consent handling.
- Updated documentation in the `schemas.rs` file to reflect changes in the data structure returned by the presets RPC.

These changes improve the onboarding experience by providing a more direct and efficient method for managing local AI presets based on device capabilities.

* test(LocalAIStep): improve mock implementation for local AI presets

- Refactored the mock for `openhumanLocalAiPresets` to enhance readability and maintainability.
- Ensured the mock returns consistent device information and preset details for testing scenarios.
- This change supports better test coverage and clarity in the LocalAIStep component's behavior during onboarding.

* fix(local_ai): preserve hadSelectedTier semantics in bootstrap return

hadSelectedTier / selectedTier represent the incoming state ("was a tier
already selected before this call"), not the post-apply state. Setting
both to the just-applied tier broke the existing localAiBootstrap
contract and the unit test that encodes it.

The Rust-side persistence fix is independent: removing the
recommend_disabled short-circuit ensures openhumanLocalAiApplyPreset
actually writes the selected tier to disk on the opt-in path, so
config_with_recommended_tier_if_unselected() honors the user's choice.
2026-04-16 10:20:28 -07:00
Steven EnamakelandGitHub 2bb20faa55 feat(threads): dedicated threads controller with per-thread session scoping (#590)
* feat(conversations): implement thread management features including creation and deletion

- Added functionality to create new conversation threads with unique IDs and titles based on the current date and time.
- Introduced a deleteThread action to remove existing threads, updating the selected thread accordingly.
- Enhanced the UI to display threads in a sidebar, allowing users to select and delete threads easily.
- Updated the Conversations component to handle thread loading and selection more efficiently, ensuring a smoother user experience.

Also updated the OpenHuman package version to 0.52.15 in Cargo.lock files.

* refactor(conversations): rename createThreadLocal to createNewThread and streamline thread creation logic

- Updated the function name from `createThreadLocal` to `createNewThread` for clarity and consistency.
- Simplified the thread creation process by removing the manual ID and title generation, leveraging the new API method for automatic thread creation.
- Adjusted the Conversations component to utilize the new `handleCreateNewThread` function, enhancing readability and maintainability.
- Removed unused thread ID and title generation functions to clean up the codebase.

This refactor improves the overall structure and clarity of the thread management functionality.

* refactor(memory): remove deprecated conversation thread management functions

- Eliminated unused functions related to listing, creating, updating, appending, and deleting conversation threads in memory operations.
- This cleanup enhances code maintainability and reduces complexity in the memory module.
- The refactor focuses on streamlining the conversation management logic, aligning with recent changes in the thread handling API.

* refactor(api): update thread API method names and remove deprecated functions

- Renamed thread-related API methods to align with the new naming convention, improving clarity and consistency.
- Removed deprecated functions related to thread creation, streamlining the API and enhancing maintainability.
- Adjusted the implementation of existing methods to reflect the updated API structure, ensuring proper functionality.

* refactor(thread): simplify thread state management and remove unused properties

- Streamlined the thread state by removing the lastViewedAt property and related logic, enhancing clarity in unread message counting.
- Updated the BottomTabBar component to reflect the new unread count logic, which now simply returns the length of threads.
- Removed the setLastViewed action from the Conversations component, further simplifying the thread management logic.
- Introduced a new deleteThread action to handle thread deletion, ensuring proper state updates and selection handling.
- Overall, these changes improve maintainability and reduce complexity in the thread management system.

* style(threads): apply cargo fmt

* refactor(tabbar): remove conversations unread badge

* update(Cargo.lock): bump OpenHuman package version to 0.52.15

* test(threadApi): update RPC method names to threads namespace

* refactor(conversations): update model ID for chat functionality

- Changed the model ID used in the chatSend function from `agentic-v1` to `reasoning-v1`, clarifying the purpose of each model.
- Added comments to explain the distinction between the reasoning model and the agentic model, enhancing code readability and maintainability.
2026-04-16 10:16:04 -07:00
CodeGhost21andGitHub ea6895f19d test(coverage): batch 5–8 — Rust unit tests toward 80% for 20 modules (#607)
* test(coverage): batch 5–8 — Rust unit tests toward 80% for 20 modules (#530)

Add 244 new unit tests across 20 modules to push line coverage from
75.06% → 75.70% overall. Modules improved:

- api/socket: 77% → 100%
- rpc/dispatch: 75% → 100%
- config/schema/channels: 77% → 100%
- composio/gmail/sync: 78% → 97%
- composio/notion/sync: 77% → 96%
- webhooks/router: 78% → 96%
- agent/hooks: 78% → 92%
- config/schema/proxy: 76% → 90%
- local_ai/device: 79% → 89%
- text_input/schemas: 73% → 89%
- agent/harness/interrupt: 76% → 88%
- billing/schemas: 79% → 87%
- screen_intelligence/schemas: 78% → 81%
- about_app/types, health/schemas, app_state/schemas,
  core/types, config/schema/identity_cost, cron/scheduler

Tests cover: schema catalog integrity, param deserialization,
serde roundtrips, helper functions, edge cases, error paths.
All 3974 tests pass.

* test(coverage): batch 9–10 — browser_open, update_memory_md, credentials profiles, cost tracker (#530)

Add 53 new tests across 4 more modules:
- browser_open: IPv4/IPv6 private ranges, host matching, normalize_domain edges
- update_memory_md: empty file, section creation, unknown action, param validation
- credentials/profiles: token expiry, CRUD operations, active profile management
- cost/tracker: budget warnings, monthly exceeded, model stats aggregation

All 4027 tests pass.

* test(coverage): batch 11–12 — channels schemas, conversation store (#530)

Add 33 new tests:
- channels/controllers/schemas: per-function input validation, param
  deserialization, helper coverage
- memory/conversations/store: thread lifecycle (create, delete, idempotent),
  multi-thread, empty/nonexistent thread, purge empty store

All 4060 tests pass.
2026-04-16 10:13:33 -07:00
7f686003a0 feat(core): gate service lifecycle on user login/logout (#582) (#603)
* refactor(voice): wrap CancellationToken in Mutex for restart support

The VoiceServer singleton uses OnceCell so it can't be recreated.
CancellationToken is one-shot — once cancelled, stays cancelled.
Wrapping it in std::sync::Mutex + adding fresh_cancel() allows
stop() then run() to work within the same process (logout → re-login).

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

* refactor(screen_intelligence): wrap CancellationToken in Mutex for restart support

Same pattern as voice server — enables stop() then run() within the
same process for logout → re-login cycles.

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

* feat(credentials): add start/stop helpers for login-gated services

Add start_login_gated_services() and stop_login_gated_services() to
credentials/ops.rs. Wire start into store_session() (after login) and
stop into clear_session() (on logout) so voice, autocomplete, screen
intelligence, and local AI are properly managed around auth lifecycle.

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

* feat(core): defer service startup behind login gate in jsonrpc.rs

Replace the unconditional service startup block with a login check.
If active_user.toml exists, start services immediately via the new
start_login_gated_services() helper. Otherwise defer until the login
handler triggers it. Autocomplete shutdown hook remains unconditional.

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

* fix(screen_intelligence): wait for Stopped state in stop() to prevent restart race

stop() now polls until the run-loop sets ServerState::Stopped (5s timeout).
Prevents fast logout→login from seeing stale Idle/Running state and
skipping the restart.

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

* fix(dictation): add stop() to prevent listener accumulation across re-logins

Store the spawned task JoinHandle and abort it on stop(). Prevents
duplicate rdev hotkey listeners from stacking across logout→login cycles.

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

* fix(credentials): call dictation_listener::stop() on logout

Wire the new dictation stop into stop_login_gated_services() so the
hotkey forwarder task is aborted when the user logs out.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 10:12:56 -07:00
a78506f6a3 fix(agent): use canonical assistant history format in subagent_runner (#606)
build_native_assistant_payload in subagent_runner.rs serialised the
assistant tool-call message using {"text": …, "tool_calls": [{
"type":"function","function":{…}}]} — two mismatches vs the format
parse::build_native_assistant_history produces and the backend
jinja template expects:

  1. "text" instead of "content" — the downstream convert_messages
     parser looks for "content" to detect assistant-with-tool-calls
     messages; "text" is not recognised, so the message is treated
     as a plain assistant message.
  2. Nested {"type":"function","function":{"name":…,"arguments":…}}
     wrappers instead of flat {"id":…,"name":…,"arguments":…}.

Result: every sub-agent that made a tool call and entered iteration
1+ hit a 400 from the backend: "jinja template rendering failed.
Message has tool role, but there was no previous assistant message
with a tool call!" — the backend saw a role=tool message without a
recognised assistant-with-tool-calls predecessor.

The fix replaces the broken inline copy with a direct call to
parse::build_native_assistant_history (the same serialiser
tool_loop.rs uses successfully for the orchestrator's tool calls).
The dead build_native_assistant_payload function is removed.

Introduced in PR #474 (6465f3d3, 2026-04-09). Latent since then
because the orchestrator self-heals by retrying via other agents.

E2E verified: skills_agent with gmail toolkit now progresses through
iterations 0→1→2 successfully (previously died at iteration 1 with
the 400 error every time).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:25:03 +05:30
c26ca795ca fix: keep chat processing alive across tab switches (#587)
* fix(chat): keep in-flight responses alive across tab switches

Made-with: Cursor

* chore: satisfy lint and format push gates

Made-with: Cursor

* fix: address CodeRabbit review on chat runtime PR

Made-with: Cursor

* feat(chat): add explicit inference turn lifecycle in chat runtime slice

Made-with: Cursor

* feat(chat): implement thinking summary feature in chat responses

Added a new mechanism to accumulate and send model reasoning text as a separate message during chat interactions. This includes handling "thinking_delta" events to gather reasoning content, formatting it for clarity, and ensuring it is sent before the main response. Updated the StreamingState struct to include a thinking accumulator for this purpose. This enhancement improves user experience by providing insight into the model's reasoning process.

* feat(telegram): update bot username handling for staging and production environments

Refactored the Telegram bot username resolution logic to differentiate between staging and production environments. Introduced constants for default usernames based on the application environment and updated the GitHub Actions workflow to set the appropriate environment variables. This change enhances the flexibility and clarity of bot username management in the application.

* fix: resolve CodeRabbit review issues on feat/thinking-telegram-summary

- bus.rs: fix UTF-8 char boundary panic in format_thinking_summary truncation
- threadSlice.ts: remove premature activeThreadId clear from addInferenceResponse.rejected
- Conversations.tsx: add composerBlocked global lock, clear tool timeline in safety timeout
- ChatRuntimeProvider.tsx: replace stale toolTimelineRef/inferenceStatusRef reads with live store.getState() calls in all event handlers
- LocalAIDownloadSnackbar.tsx: reset dismissed/collapsed on not-downloading → downloading transition edge
- MemoryGraphMap.tsx: derive activeSelectedNode to guard stale selectedNode after relations refresh; add debug logs to useMemo graph recompute

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

* fix: use render-phase update for download dismiss reset in LocalAIDownloadSnackbar

Replace effect-based setState with the React render-phase update pattern so
the not-downloading → downloading transition resets dismissed/collapsed without
triggering the react-hooks/set-state-in-effect lint warning.

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

* chore: apply prettier and cargo fmt auto-formatting

Formatting changes applied by the pre-push hook during the previous commit.

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

* refactor: replace endInferenceTurn with clearRuntimeForThread in Conversations component

Updated the Conversations component to replace the endInferenceTurn dispatch with clearRuntimeForThread. This change simplifies the handling of thread runtime state during error scenarios and timeout conditions, ensuring a cleaner state management approach.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 06:27:48 +05:30
CodeGhost21andGitHub a8664f066f test(coverage): batch 1–4 — Rust unit tests toward 80% for critical modules (#530) (#581)
* test(coverage): phase 1 — webhooks/types, workspace/ops, webhooks/schemas

Lines: webhooks/types 0% → 100%; workspace/ops 0% → 96.98%;
webhooks/schemas 35.40% → 79.18%.

- webhooks/types.rs: serde round-trip tests for WebhookRequest /
  WebhookResponseData / TunnelRegistration (including the
  default_webhook_target_kind fallback) / WebhookActivityEntry /
  WebhookDebugLogEntry / the three debug result wrappers / and
  WebhookDebugEvent, exercising every `#[serde(default)]` and
  camel-case rename.
- workspace/ops.rs: ensure_workspace_file covers create / leave / force
  / write-error branches; BOOTSTRAP_FILES contract test locks in SOUL
  and IDENTITY presence; init_workspace covers fresh-install, idempotent
  second-call, and forced-overwrite paths against a temp OPENHUMAN_WORKSPACE
  (serialised via the shared TEST_ENV_LOCK).
- webhooks/schemas.rs: catalog integrity (length / namespace /
  uniqueness / schema↔handler parity), per-function required-field
  assertions for all 11 RPC methods plus the unknown fallback, and
  deserialize_params / json_output / to_json coverage.

Also fixes a pre-existing test bug in composio/ops.rs discovered while
running llvm-cov: fetch_connected_integrations_via_mock_aggregates_tools
was not mocking /composio/toolkits, so list_toolkits failed and the
expected aggregation never happened. Adds the missing route so the
test now observes the 2-integration result it asserts.

* test(coverage): phase 2 — webhooks/bus, webhooks/ops

Lines: webhooks/bus 0% → 100%; webhooks/ops 14.34% → 97.96%.

- webhooks/bus.rs: base64_encode / error_body helpers; Default vs new
  equivalence; EventHandler name and domain filter; handle() on a
  non-webhook variant (early return) and on a WebhookIncomingRequest
  without a registered socket manager (graceful fallthrough).
- webhooks/ops.rs: require_token for missing / whitespace / valid
  stored sessions; the four stub RPC ops (list_registrations, list_logs
  including ignored-limit, clear_logs, register/unregister_echo) lock
  in their current payload + log shape; build_echo_response decodes
  back to the expected echo body and sets the echo-target header;
  trimmed-input validation for create_tunnel (empty/whitespace name)
  and the id-bearing ops (get/update/delete); and full mock-backend
  round-trips for list_tunnels, create_tunnel (trim + drop-whitespace
  description), get_tunnel, update_tunnel, delete_tunnel, and
  get_bandwidth, plus a guard asserting authed HTTP calls fail fast
  without a session token.

* test(coverage): phase 3 — voice/postprocess, voice/text_input

Lines: voice/postprocess 58.94% → 92.06%; voice/text_input 18.83% → 51.18%.

voice/postprocess.rs
- Convert existing short-circuit tests to #[tokio::test] so the async
  function is awaited directly instead of via a freshly-constructed
  runtime per case.
- Add `enabled_but_llm_not_ready_returns_raw_text` for the "cleanup is
  on but the local LLM hasn't reached ready/degraded yet" branch.
- Add five LLM-ready tests that spin up a mock Ollama behind the
  OPENHUMAN_OLLAMA_BASE_URL override: happy-path cleanup + trim,
  whitespace-only response fallback, HTTP 500 fallback, conversation
  context embedded in the prompt, and whitespace-only context
  ignored. Assertions are permissive about "LLM called → cleaned" vs
  "LLM short-circuited → raw" because ~30 sibling tests touch the
  shared `local_ai::global()` singleton without LOCAL_AI_TEST_MUTEX
  and can race our `state = "ready"` setup. Either branch is
  documented as acceptable; the function must always return a
  deterministic String and never panic. Full end-to-end correctness
  of the cleanup output is pinned by the deterministic short-circuit
  tests above.

voice/text_input.rs
- Add `\\t` / `\\n`-only input case and verify the OpenWhispr timing
  constants (PASTE_DELAY 120ms, CLIPBOARD_RESTORE_DELAY 450ms) so
  nobody silently shortens them and breaks paste reliability.
- macOS: escape_applescript_string backslash/quote/idempotence cases
  and restore_focus_to_app error path against a bogus app name.
- Ceiling note: the clipboard/enigo body of insert_text is not
  testable in a headless environment (Clipboard::new / Enigo::new
  fail without a display). Pushing past ~51% here requires
  dependency-injecting those traits — a production refactor, not a
  test addition.

* test(coverage): batch 4.1 — skills/bus, text_input/{types,ops}

Lines: skills/bus 0% → 100%; text_input/types 0% → 100%;
text_input/ops 0% → 57.67% (accessibility-gated ceiling).

- skills/bus.rs: idempotent no-op for the legacy
  register_skill_cleanup_subscriber() hook.
- text_input/types.rs: FieldBounds ↔ accessibility::ElementBounds
  round-trip, ReadFieldParams default/omitted-key serde, and
  round-trips for every request/result struct (InsertText,
  ShowGhostText, DismissGhostText, AcceptGhostText).
- text_input/ops.rs: empty-text guard assertions for insert_text,
  show_ghost, and accept_ghost; dismiss_ghost idempotent-success
  contract; plus a deterministic-shape check that the accessibility
  failure path wraps the error into InsertTextResult rather than
  panicking. Anything past the guard reaches `accessibility::*`,
  which requires a live focused field on an OS display and is
  therefore not reachable in a headless unit-test environment —
  lifting the ceiling past ~58% requires dependency-injecting the
  accessibility surface, a production refactor.

* test(coverage): batch 4.2 — service/bus, migration/ops, referral/ops

Lines: service/bus 0% → 85.25%; migration/ops 0% → 89.71%;
referral/ops 0% → 94.63%.

- service/bus.rs: RestartSubscriber name and domain metadata, plus
  two handle() branches that are safe to exercise — non-restart
  event (early return) and duplicate-suppression (gate already set).
  Deliberately does NOT cover the success path of a real
  SystemRestartRequested — it spawns a tokio task that calls
  std::process::exit(0) and would terminate the test runner.
  register_restart_subscriber idempotency test confirms the
  OnceLock guard skips re-registration.
- migration/ops.rs: dry-run against an empty temp workspace returns
  the canonical "migration completed" log; missing source-workspace
  path exercises the Err-propagation branch.
- referral/ops.rs: require_token for missing / whitespace / valid
  sessions; get_stats + claim_referral guard fast-fail without a
  session; claim_referral round-trip against a mock backend
  asserting (a) the referral code is trimmed, (b) a whitespace-only
  deviceFingerprint is dropped from the outgoing body, and (c) a
  non-empty fingerprint is trimmed and forwarded.

* test(coverage): batch 4.3 — security/ops, service/{bus,ops}, update/{ops,scheduler}

Adds unit tests covering:
- security/ops: security_policy_info payload shape + default values
- service/ops: daemon_host_get/set happy path and error branches
- service/bus: fix register_restart_subscriber test to use #[tokio::test]
  so it has a runtime under `cargo llvm-cov`
- update/ops: validate_asset_name and validate_download_url edge cases,
  update_apply short-circuit guards
- update/scheduler: min-interval invariant, disabled-run short-circuit,
  tick resilience when the event bus is uninitialised

All 3583 lib tests pass under `cargo llvm-cov`. Refs #530.

* style: cargo fmt cleanup in coverage test modules

Auto-fixes from `cargo fmt` on test code added in batches 4.1–4.3.
No behavioral changes.

* test(coverage): batch 5.1 — cron/{ops,schemas}

- cron/ops.rs: 74.73% → 91.20% (+add_once_at, pause/resume, async cron_list/update/remove/runs, enabled/disabled and empty-id branches)
- cron/schemas.rs: 29.11% → 77.00% (all schemas() branches, registry helpers, read_required/read_optional_u64/type_name)

30 new deterministic tests. Refs #530.

* test(coverage): batch 5.2 — memory/{rpc_models,schemas}

- memory/rpc_models.rs: 70.91% (targets resolved_limit priority across QueryNamespace/RecallContext/RecallMemories, deny_unknown_fields enforcement, ApiError/ApiMeta/ApiEnvelope round-trips)
- memory/schemas.rs: 45.67% (all 31 controller schemas present, registry parity with handlers, parse_params success + error paths, unknown function placeholder)

19 new deterministic tests. Refs #530.

* test(coverage): batch 5.3 — socket/manager webhook router paths

- socket/manager.rs: 67.54% (adds set_webhook_router populates/overwrites paths and emit-after-disconnect guard)

3 new deterministic tests. Refs #530.

* test(coverage): batch 5.4 — config/{schemas, schema/observability}

- config/schemas.rs: 52.37% (adds required_string / optional_string / optional_bool field builder coverage, exercises deserialize_params across ModelSettings / MemorySettings / WorkspaceOnboarding / SetBrowserAllowAll / OnboardingCompleted params, pins DEFAULT_ONBOARDING_FLAG_NAME constant)
- config/schema/observability.rs: 66.67% (default-values invariant, serde defaults for optional fields, explicit analytics flag, round-trip)

16 new deterministic tests. Refs #530.

* test(coverage): batch 5.5 — local_ai/{core,gif_decision}

- local_ai/core.rs: 33.33% (pins model_artifact_path structure: models/local-ai dir, `.ollama` suffix, colon→dash normalisation for Windows-safe filenames; Arc sharing across global() calls)
- local_ai/gif_decision.rs: 44.04% (trim whitespace in parse, length/word-count boundary cases, tenor_search empty-query guard, local_ai_should_send_gif empty-message early return)

10 new deterministic tests. Refs #530.

* test(coverage): batch 5.6 — local_ai/sentiment, migration/schemas, referral/schemas

- local_ai/sentiment.rs: 68.03% (negative-confidence clamp to zero, unknown valence fallback, all documented emotion/valence labels accepted, neutral() constructor invariants, empty-message early return)
- migration/schemas.rs: 36.73% (controller registry parity, openclaw input shape, MigrateOpenClawParams defaults + round-trip, unknown function placeholder, to_json wrapping)
- referral/schemas.rs: 37.18% (controller registry parity, claim input required/optional mapping, ReferralClaimParams camelCase alias + missing-code rejection, json_output + to_json helpers)

25 new deterministic tests. Refs #530.

* test(coverage): batch 5.7 — tools/traits, local_ai/device

- tools/traits.rs: 76.77% (Tool default-method values for permission/scope/category, PermissionLevel total order + default + Display + serde round-trip, ToolCategory default + Display + snake_case serde, ToolScope variant distinctness)
- local_ai/device.rs: 63.16% (total_ram_gb truncation for sub-GB and partial-GB, detect_gpu branches for Apple-brand / ARM-on-mac / Intel-Mac, DeviceProfile serde round-trip)

17 new deterministic tests. Refs #530.

* test(coverage): address PR review — tighten assertions, fix flaky test, add env-var RAII guard

Inline review fixes:
- migration/ops.rs: migrate_openclaw_returns_error_for_missing_source_workspace now requires Err() (the underlying helper bails when the source dir doesn't exist) and asserts a non-empty error message.
- text_input/ops.rs: replace tautological `!inserted || inserted` in insert_text_surfaces_accessibility_failure_as_inserted_false with the real contract: require Ok(..) and pin `inserted`↔`error` mutual exclusion. Headless runs legitimately see inserted=false, so the assertion still holds while catching regressions in either branch.
- update/scheduler.rs: remove tick_runs_without_panicking_when_event_bus_is_uninitialised — it hit real api.github.com HTTPS and was flaky under offline CI / rate limits. Replace with a comment documenting the decision and the integration-test path for exercising tick().

Nitpick fixes:
- security/ops.rs: extend security_policy_info_matches_default_policy_values with assertions for autonomy and allowed_commands so the full default shape is pinned.
- voice/postprocess.rs: tighten ready_llm_with_whitespace_only_context_never_embeds_header from `assert_eq!(result.trim(), "raw text")` to exact equality (cleanup_transcription trims internally). The pre-existing doc comment on with_ready_llm plus the in-module block comment at lines 255-268 already document the LOCAL_AI_TEST_MUTEX contract and the deliberate non-use in permissive tests — no new docs needed.
- webhooks/ops.rs: list_tunnels_hits_webhooks_core_endpoint_and_returns_payload now asserts the inbound Authorization header equals `Bearer test-session-token`; get_tunnel_encodes_id_in_path uses an id full of reserved URL chars so the test actually verifies percent-encoding rather than just trimming.
- workspace/ops.rs: introduce a WorkspaceEnvGuard RAII helper; replace all six unsafe set_var/remove_var pairs in the init_workspace tests so OPENHUMAN_WORKSPACE is cleared on panic too. Contract is documented inline: guard requires the caller to hold ENV_LOCK.

Refs #530.

* test(coverage): tighten sentry_dsn round-trip assertion to exact value

round_trip_preserves_all_fields previously only checked
`back.sentry_dsn.is_some()`, which would still pass if serde dropped or
corrupted the DSN string. Compare the full decoded value instead so any
regression in the Option<String> path is caught.

Refs #530.
2026-04-15 16:31:51 -07:00
19aa50a429 fix(agent): thread agent_id into build_session_agent_inner (#584)
build_session_agent_inner took an agent_id: &str parameter but
never threaded it into the Agent::builder() chain — a `let _ =
agent_id;` silenced the unused-variable warning. AgentBuilder::build()
fell back to the legacy "main" default at builder.rs:310-312, so
every session built via Agent::from_config_for_agent carried
agent_definition_name="main" regardless of the id the caller asked
for.

Only two ids reach this path in production: "orchestrator" (legacy
Agent::from_config wrapper, cron, local_ai, escalation) and
"welcome" (channels/providers/web.rs routing after #525/#526, and
welcome_proactive). Orchestrator is benign — "main" was already
an alias for orchestrator everywhere downstream (see debug_dump.rs:249).
Welcome is the visible bug — agent_definition_name feeds three
surfaces that are all silently wrong pre-fix:

  1. Transcript filename — welcome sessions land as main_*.md
     instead of welcome_*.md.
  2. Transcript metadata — the <!-- session_transcript --> block
     stamps `agent: main` instead of `agent: welcome`.
  3. Transcript resume cross-contamination —
     try_load_session_transcript calls
     find_latest_transcript(workspace_dir, "main") which scans all
     dated session dirs for the latest main_*.md. It finds the
     last orchestrator session and resumes from it, loading its
     system prompt + user messages + assistant tool-call history
     into the welcome agent's `history` as a resume prefix before
     run_single runs. The written transcript mixes yesterday's
     orchestrator email thread with today's welcome message under
     the same main_0.md filename. Reproduced live 2026-04-16: a
     welcome_proactive session loaded 5 messages from yesterday's
     15042026/main_0.md and wrote 6 messages back that included a
     spawn_subagent(skills_agent, toolkit=gmail) tool-call the
     welcome agent never made.

Prompt rendering is NOT affected. context/prompt.rs only reads
ctx.agent_id for the is_skill_executor branch at prompt.rs:655, so
welcome/main/orchestrator all fall through the same delegator path.
The welcome persona prompt is rendered by the stamped
SystemPromptBuilder::for_subagent(target_def.system_prompt) built
at session-build time, independent of agent_definition_name. The
pre-fix main_0.md on disk contains `# Welcome Agent\n\nYou are the
Welcome agent...` as its system prompt body — correct content,
wrong filename and metadata.

skills_agent and other typed sub-agents are unaffected — they go
through subagent_runner, which constructs its prompt directly and
writes transcripts under the explicit id it receives. The
pre-existing sessions/15042026/skills_agent_0.md on disk with
`agent: skills_agent` confirms this code path has always been
correct.

Changes:

  * src/openhuman/agent/harness/session/builder.rs:
    - Add .agent_definition_name(agent_id.to_string()) to the
      builder chain in build_session_agent_inner.
    - Delete the `let _ = agent_id;` suppression.
    - Replace the misleading 8-line comment block at the call site
      (which claimed event_channel was for transport identity and
      therefore stamping agent_id wasn't needed — conflating two
      unrelated fields) with an accurate description of the three
      load-bearing surfaces.
    - Expand the docstring on AgentBuilder::agent_definition_name
      to document the surfaces and the latent prompt-section
      foot-gun the fix closes for future code.
    - New log::debug! call at the stamping site for grep-friendly
      runtime traces ([agent::builder] stamping
      agent_definition_name=<id> onto session agent).

  * src/openhuman/agent/harness/session/runtime.rs:
    - Add pub fn agent_definition_name(&self) -> &str accessor
      on Agent so tests and runtime callers can introspect the
      stamped id without reaching into the pub(super) field.

  * src/openhuman/agent/harness/session/tests.rs:
    - Add build_minimal_agent_with_definition_name helper.
    - agent_builder_threads_agent_definition_name_when_set —
      parameterised over welcome/skills_agent/orchestrator/
      trigger_triage, asserts the setter threads the id through.
    - agent_builder_falls_back_to_main_when_definition_name_unset
      — pins the legacy fallback contract direct builder users rely
      on.

Tests: 2 passed; 0 failed; 3477 filtered out; finished in 0.21s.
cargo check --lib clean; cargo fmt clean.

Verified live against G:/projects/vezures/.openhuman on 2026-04-16:
  - Pre-fix welcome_proactive run wrote sessions/16042026/main_0.md
    with `agent: main` in the metadata header.
  - Post-fix welcome_proactive run wrote sessions/16042026/welcome_0.md
    with `agent: welcome` in the metadata header.
  - Post-fix web-chat dispatch to orchestrator wrote
    sessions/16042026/orchestrator_0.md (moved off the historical
    main_*.md alias — behaviorally unchanged for orchestrator).
  - The new [agent::builder] stamping debug line fires on both
    welcome and orchestrator paths.

Does not touch: subagent_runner, spawn_subagent / dispatch_subagent,
any agent/agents/*/agent.toml, any context::prompt code, or the
AgentBuilder::build() fallback itself.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:31:36 -07:00
d4f5b9a357 fix(overlay): fullscreen visibility, voice server reliability, and resize (#528) (#585)
* fix(overlay): shrink initial overlay window to match idle orb dimensions (#528)

The Tauri overlay window was 248×228 px while the idle orb renders at
50×50. The excess transparent area wasted compositing resources and
created an invisible click-absorbing region. Reduce to 60×60 to tightly
frame the idle orb with minimal padding.

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

* feat(overlay): add native drag support with position persistence (#528)

- Mouse-down on the orb initiates Tauri startDragging() for native
  window drag
- Dragged position is saved to localStorage and survives mode changes
  (idle ↔ active) so the orb stays where the user placed it
- Double-click resets to the default bottom-right corner
- Cursor changes to grab/grabbing for affordance
- Skip default repositioning when a saved position exists

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

* fix(overlay): reclass NSWindow to NSPanel for fullscreen visibility (#528)

macOS fullscreen apps run in separate Spaces where standard NSWindow
cannot follow. Use object_setClass() to reclass the Tauri overlay
window from NSWindow to NSPanel at runtime, then configure it with
NonactivatingPanel style mask and Transient collection behavior —
matching the working Swift accessibility helper pattern.

Key configuration that makes this work:
- object_setClass(NSWindow → NSPanel) — in-place reclass, no reparenting
- NSWindowStyleMask::NonactivatingPanel — critical for panel behavior
- NSWindowCollectionBehavior::Transient (not Stationary) — follows Spaces
- Window level 25 (NSStatusWindowLevel) — floats above fullscreen apps
- setFloatingPanel(true), setHidesOnDeactivate(false)

Previous approaches that failed:
1. CGShieldingWindowLevel + CanJoinAllSpaces — hidden (NSWindow limitation)
2. Window level i32::MAX-17 + Stationary — hidden (Space membership issue)
3. CGS private API CGSSetWindowTags sticky bit — blocked on Sonoma
4. object_setClass WITHOUT NonactivatingPanel mask — hidden
5. Create new NSPanel + reparent webview — CRASH (Tao delegate panic)

Also removes unused objc2-core-graphics and objc2-foundation deps.

Ref: https://github.com/tauri-apps/tauri/issues/11488

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

* fix(sidecar): make dev signing non-fatal in stage script

codesign failures no longer call process.exit(), preventing
yarn tauri dev from hanging when the dev signing identity is
missing or the keychain rejects the request.

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

* fix(voice): prevent race condition and fix restart after stop

- Atomically transition Stopped → Idle at start of run() to prevent
  duplicate run() calls during slow globe listener compilation
- Wrap CancellationToken in Mutex so run() creates a fresh token on
  each start — a cancelled token cannot be reused after stop()
- Reset state to Stopped if hotkey listener fails to start

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

* fix(voice): capture server errors from spawned run() task

Store errors from the background server.run() task via
set_last_error() so they surface in voice_server_status RPC
responses instead of being silently lost.

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

* fix(overlay): enable programmatic resize and shrink idle dimensions

- Change overlay window from 60x60 to 50x50 to match idle orb size
- Remove minWidth/minHeight constraints that blocked dynamic resize
- Set resizable: true so setSize() calls work for bubble expansion

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

* fix(overlay): simplify window resize and bubble rendering

- Clear min/max constraints before resizing to avoid clamping
- Replace CSS transition-based bubble visibility with conditional
  mount for more reliable rendering when mode changes

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

* chore: fix fmt and remove unused bubbles variable

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

* fix(voice): enforce lock ordering to prevent race between run() and stop()

Acquire cancel lock before state lock in run() — same order as stop() —
so stop() cannot cancel a stale token between setting Idle and swapping.

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

* fix(overlay): restore saved position on resize and format with prettier

Parse and apply saved drag coordinates instead of just using their
presence as a sentinel. Also reformats for prettier compliance.

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

* fix(sidecar): fail-fast on dev signing failure in CI environments

Add CI detection so signing failures abort the build in CI but remain
non-fatal for local development.

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

* chore: fix cargo fmt on Tauri shell import

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 04:52:14 +05:30
f014058417 feat(local_ai): MVP model lockdown — lock selection to 2-4 GB tier (#573) (#588)
* feat(local_ai): add MVP tier ceiling and cap model recommendation (#573)

Introduce MVP_MAX_TIER constant (Ram2To4Gb), is_mvp_allowed() gate,
mvp_presets() filter, and cap recommend_tier() so auto-provisioning
never selects a model above the MVP ceiling regardless of device RAM.

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

* feat(local_ai): enforce MVP model allowlists on resolved IDs (#573)

Add per-category allowlists (chat, vision, embedding) so that
effective_*_model_id() silently redirects any non-MVP model to the
default. Prevents config-file edits from bypassing the 2-4 GB tier
restriction.

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

* feat(local_ai): reject non-MVP tiers in RPC and clamp at bootstrap (#573)

apply_preset handler now returns an error for tiers above the MVP
ceiling. Bootstrap clamps any existing out-of-range tier selection
down to the recommended (capped) preset on startup.

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

* feat(ui): lock model tier selection and show full roadmap (#573)

Replace clickable tier buttons with static cards. Active tier shows
"Active" badge; locked tiers show "Coming soon" with reduced opacity.
Add MVP info banner. Fix download size to 1 decimal place.

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

* fix(local_ai): resolve CI failures — fmt, unused props, dead code (#573)

Apply cargo fmt to single-element array constants. Remove unused
isApplyingPreset/onApplyPreset props and applyPreset function from
the settings panel since tier switching is disabled for MVP.

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

* style: apply Prettier formatting to settings panels (#573)

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 04:50:28 +05:30
YellowSnnowmannandGitHub cca6c08ab0 Fix: discord (#580)
* feat(discord): implement managed link functionality and enhance DiscordConfig component

- Added support for managed Discord linking, including new API methods for initiating and checking link status.
- Enhanced the DiscordConfig component to manage link tokens and implement polling for link completion.
- Introduced new state management for link tokens and improved error handling during connection processes.
- Updated the definitions to include a new auth action for managed links, streamlining the onboarding experience for users.
- Added tests for the new Discord link functionality to ensure robust integration and error handling.

This commit significantly improves the user experience for connecting Discord accounts, providing clearer feedback and handling for managed links.

* feat(discord): implement managed link functionality and enhance DiscordConfig component

- Added support for managed Discord linking, including new API methods for initiating and checking link status.
- Enhanced the DiscordConfig component to manage link tokens and implement polling for link status, improving user experience during the linking process.
- Introduced new state management for link tokens and error handling, ensuring robust interaction with the Discord API.
- Updated the channel definitions to include a new auth action for managed links, streamlining the integration process.
- Added tests for the new API methods and updated existing tests to cover the new functionality.

This commit significantly improves the integration with Discord, allowing users to link their accounts more effectively and providing better feedback during the linking process.

* chore(release): update OpenHuman version to 0.52.9 in Cargo.lock

- Bumped the version of the OpenHuman package from 0.52.7 to 0.52.9 in both Cargo.lock files for the main application and the Tauri app.
- This update ensures that the latest features and fixes from the OpenHuman package are included in the project.

* fix(tests): update DiscordConfig test to reflect new Connect button count

- Adjusted the test for the DiscordConfig component to expect three Connect buttons instead of two, aligning with recent changes in the component's authentication modes.
- This update ensures that the test accurately reflects the current functionality and improves test reliability.

* refactor(discord): modularize channel permission checks and enhance logging

- Extracted the `check_channel_permissions_at_base` function to modularize the permission checking logic for better readability and maintainability.
- Updated the API calls to use a dynamic base URL instead of a hardcoded one, improving flexibility.
- Enhanced logging for non-success responses from Discord API calls, providing more context for debugging.
- Minor adjustment in the hallucination check logic to improve clarity in variable usage.

* fix(logging): enhance error handling in DiscordConfig and SocketProvider

- Updated error handling in DiscordConfig to log specific errors when link checks fail, improving debugging capabilities.
- Enhanced SocketProvider to log non-fatal RPC connection failures, providing clearer context for potential issues with the sidecar or backend connectivity.

* feat(discord): add validation functions for Discord link start and complete responses

- Introduced `expectDiscordLinkStart` and `expectDiscordLinkComplete` functions to validate the structure of responses from Discord link API calls.
- Updated `channelConnectionsApi` methods to utilize these new validation functions, improving error handling and response consistency.
- Added tests to ensure proper unwrapping and validation of Discord link responses, enhancing overall reliability of the API integration.

* fix(channelConnections): improve handling of lastError and capabilities in touchConnection function

- Updated the touchConnection function to conditionally assign lastError and capabilities based on their presence in the patch object, ensuring more accurate state management.
- This change enhances the reliability of connection updates by preventing unintended overwrites of existing values.

* test(channelConnections): add test to clear lastError when explicitly set to undefined

- Introduced a new test case to verify that the lastError state is cleared when an explicit patch sets it to undefined. This enhances the reliability of the state management in the channelConnectionsSlice reducer.

* test(tests): add tests for upstream_unhealthy detection and failure reason precedence

- Introduced multiple test cases to verify the detection of upstream unavailability and service unavailability errors.
- Ensured that the `failure_reason` function correctly prioritizes upstream_unhealthy over other error states, enhancing the reliability of error handling in the system.

* feat(discord): add OAuth success handling in DiscordConfig component

- Implemented a new useEffect to handle successful OAuth events for Discord, updating the channel connection status and capabilities accordingly.
- This enhancement improves the integration with Discord by ensuring that the application responds correctly to OAuth success events, providing a better user experience.

* feat(deepLink): enhance OAuth deep link handling to include skillId

- Updated the deep link handling logic to also retrieve the skillId from the URL parameters, improving flexibility in OAuth success scenarios.
- Added a new simulation example for skillId in the setupDesktopDeepLinkListener function to aid in testing and development.

* refactor(format): ran format

* fix(discord): update permissions mock and improve message dispatch test

- Revised the permissions mock to include additional endpoints for better accuracy in testing Discord API interactions.
- Adjusted the message dispatch test to utilize a deterministic stub for the agent's response, ensuring consistent timing and behavior during tests.
- Reduced the delay in the SlowProvider to enhance test performance while maintaining robustness.

* chore(dependencies): update OpenHuman package version to 0.52.13 in Cargo.lock
2026-04-15 22:59:28 +05:30
d545c193b9 feat(agent): fuzzy-filter skills_agent toolkit actions by task prompt (#579)
* feat(agent): fuzzy-filter skills_agent toolkit actions by task prompt

Narrow large Composio toolkits (e.g. github ~500 actions) down to the
handful relevant to a given delegation prompt before registering them
as native tools on a spawned skills_agent. Falls back to the full
catalogue when the filter yields fewer than MIN_CONFIDENT_HITS hits to
avoid starving the sub-agent on under-specified prompts.

Filter is only invoked when both `definition.id == "skills_agent"` and
a `toolkit=` argument is present, so orchestrator and other sub-agents
are unaffected.

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

* test(composio): mock toolkits route in ops integration test

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:06:59 +05:30
ea088d9f15 feat(prompt): unified delegation guide + dynamic per-toolkit tool registration (#447) (#570)
* feat(prompt): unified delegation guide + dynamic per-toolkit tool registration (#447)

Cuts main agent prompt token cost ~98% on accounts with connected
integrations and replaces the generic Composio dispatcher with native
per-action tool calling inside skills_agent.

## Why

Issue #447: prompt payloads were ballooning because main/orchestrator
re-shipped a full prose enumeration of every connected toolkit's
actions on every turn (~13.7k tokens for one gmail integration alone),
and skills_agent had no way to scope tools to a single toolkit so it
inherited a flat dispatcher (composio_execute) that the LLM couldn't
reliably call without first round-tripping composio_list_tools.

## What

### main / orchestrator prompt
- Replace `ConnectedIntegrationsSection`'s per-action prose dump with
  a unified Delegation Guide: one bullet per integration (toolkit +
  one-line description + spawn snippet). Tokens added per
  integration: ~30. Savings vs old per-action listing: ~5k+ tokens
  per connected toolkit.
- Drop every "Composio" reference from delegating-agent prose so the
  surface stays provider-neutral.

### skills_agent
- Add a `toolkit` argument to `spawn_subagent` with three pre-flight
  validation modes resolved against the parent's connected
  integrations list before any LLM call:
    * missing toolkit         -> ToolResult::error (model retries)
    * unknown toolkit         -> ToolResult::error (model retries)
    * in allowlist, not connected -> ToolResult::success (model
      surfaces "authorize in Settings -> Integrations" to user; not
      flagged as a tool failure in the agent loop or web channel)
- New `ComposioActionTool` (`src/openhuman/composio/action_tool.rs`)
  implements the `Tool` trait per Composio action. The sub-agent
  runner constructs ~N of these at spawn time from the cached
  integration overview and injects them via a new `extra_tools`
  parameter through `run_typed_mode` -> `run_inner_loop`.
- `filter_tool_indices` drops every skill-category parent tool when
  `is_skills_agent_with_toolkit` is true so the only skill-category
  entries the sub-agent sees are the freshly-built per-action tools.
  This eliminates apify_*, composio_list_*, composio_authorize, and
  composio_execute from the toolkit-scoped surface.
- Sub-agent renderer takes the parent's actual `tool_call_format`
  instead of hardcoding PFormat. Native dispatchers no longer carry a
  prose `## Tools` section at all (schemas already travel through the
  request body's `tools` field) — eliminates a ~30k-token duplication
  that was blowing past the model's context window.

### integration overview fetch
- `fetch_connected_integrations_uncached` now merges Composio's
  toolkit allowlist with the user's active connections and returns
  one `ConnectedIntegration` per allowlisted toolkit with a
  `connected: bool` flag. Unconnected entries carry no schemas, just
  the toolkit name + description, so the orchestrator can mention
  them without trying to invoke them.
- `ConnectedIntegrationTool` preserves the action's full JSON
  parameter schema so `ComposioActionTool` can advertise it through
  native function-calling.

### plumbing
- `ParentExecutionContext` carries a `ComposioClient` and the
  parent's resolved `tool_call_format`, populated alongside
  `connected_integrations` in `Session::fetch_connected_integrations`.
  Triage and test paths pass `None` / `PFormat` defaults.
- `dispatch_subagent` (the `SkillDelegationTool` path) plumbs its
  pre-bound skill_id through `toolkit_override` instead of the
  broken `skill_filter_override` (which used `{skill}__` prefix
  matching that never matched Composio's `TOOLKIT_*` naming).
- `web::run_chat_task` failures now log the underlying error at WARN
  so debugging an in-flight failure no longer requires turning on
  TRACE for socket events.
- `scripts/stage-core-sidecar.mjs` queries `cargo metadata` for the
  real target directory instead of assuming `<repo>/target` so the
  staging step works under workspace-level `target-dir` overrides
  (the vezures-workspace shared `.cargo-target` setup).

## Verified end-to-end

Two RPC tests against a freshly-rebuilt sidecar (gmail connected,
notion / slack / etc. allowlisted but not connected):

| Test | Behavior | Tokens | Iterations |
|---|---|---|---|
| Connected (gmail, "fetch 5 unread emails") | spawn_subagent -> 62 dynamic gmail tools registered -> 1 GMAIL_FETCH_EMAILS call with smart args -> markdown table response | 42,911 input | 1 |
| Not-connected (notion, "create a page") | main answers directly without spawning, tells user to authorize in Settings -> Integrations | 3,319 input | 1 |

Both flows complete cleanly. The connected path proves dynamic
per-action tool registration is working with full schema validation;
the unconnected path proves the unified delegation guide + ToolResult::success
return for not-connected toolkits keeps the model on a graceful path
without polluting the chat with error styling.

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

* test(prompt): update test callers + assertions for new renderer signature

Follow-up to #447. The main patch changed three signatures that test
callers hadn't been updated for, and flipped one assertion that was
validating the now-removed prose schema duplication.

- `SubagentRunOptions` test constructors in subagent_runner.rs (2
  sites) now pass `toolkit_override: None`.
- `ConnectedIntegration` test constructor in orchestrator_tools.rs
  now passes `connected: true` (the default for test integrations —
  they're treated as authorized so delegation logic still runs).
- 12 `render_subagent_system_prompt` test callers in prompt.rs now
  pass `&[]` for the new `extra_tools` slice and
  `ToolCallFormat::PFormat` for the new `tool_call_format` argument.
- `render_subagent_system_prompt_honors_identity_safety_and_skills_flags`
  used to assert `rendered.contains("Parameters:")` on the Json
  dispatcher branch — that was valid in the old world where the
  prose `## Tools` section dumped full JSON schemas for Json/Native
  formats. The main patch deliberately removes that dump (it was the
  ~30k-token duplication of the native `tools` field), so the test
  now asserts the opposite: no `## Tools` header and no `Parameters:`
  line are emitted for Native/Json dispatchers. The schemas still
  travel through the provider request's `tools` field.

Also picks up `rustfmt` rewraps in action_tool.rs and ops.rs from a
background linter run — pure whitespace, no semantic change.

Verified green against the full `cargo test --lib` suite for every
test touched by this PR:
- openhuman::context::prompt::tests                         (26 passed)
- openhuman::agent::harness::subagent_runner::tests         (19 passed)
- openhuman::composio::ops::tests                           (2 passed)
- openhuman::tools::impl::agent::tests                      (0 scoped)

The 7 remaining failures in `cargo test --lib` are pre-existing
Windows-path/filesystem flakes in subsystems this PR doesn't touch
(self_healing polyfill path separator, cron scheduler shell spawning,
local_ai::paths absolute-path detection, security::policy sandbox
path handling, composio::trigger_history jsonl archive, and a real
pre-existing `Option::unwrap()` panic in browser::screenshot).

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

* test(harness): add composio_client + tool_call_format to integration test stub

Follow-up to #447. The `tests/agent_harness_public.rs` integration
test constructs a `ParentExecutionContext` in `stub_parent_context`
that was missing the two fields added in the main patch
(`composio_client` and `tool_call_format`). `cargo test --lib`
doesn't compile integration tests, which is why this slipped past
local verification — it only surfaced on Linux CI.

- `composio_client: None` — the stub parent has no composio client
  because these tests don't exercise the integration-overview path.
- `tool_call_format: ToolCallFormat::PFormat` — default legacy
  format; none of the tests in this file exercise the sub-agent
  renderer's format branching, so PFormat is the safe pick.

Verified locally that `cargo test --no-run` compiles all integration
test targets including `agent_harness_public`. (The
`agent_memory_loader_public` link error in the local run is a
pre-existing Windows `libucrt`/`fgets` C-runtime issue unrelated to
this PR — won't reproduce on Linux CI.)

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

* fix(prompt,composio): address CodeRabbit review on #570 (#447)

Four fixes from the CodeRabbit review of PR #570, ranked by impact:

## `context/prompt.rs` — Json dispatcher needs prose tool catalog

The initial patch gated the prose `## Tools` section behind
`matches!(tool_call_format, ToolCallFormat::PFormat)`, which
conflated Json with Native. Only `ToolCallFormat::Native` uses the
provider's native function-calling channel where schemas travel via
the request body's `tools` field. `ToolCallFormat::Json` is a
prompt-driven format (the model wraps JSON tool calls in
`<tool_call>` tags) and relies on the prose catalog the same way
PFormat does — without it the model sees protocol instructions but
no visible tool names or schemas.

Inverted the guard to `!matches!(tool_call_format, Native)` so
PFormat and Json both render the `## Tools` section with their
respective per-entry shapes (compact `Call as:` signature for
PFormat, inline `Parameters:` JSON schema for Json). Updated the
`render_subagent_system_prompt_honors_identity_safety_and_skills_flags`
test: the Json assertion now expects `Parameters:` to be present
again (reverting commit 2's flip), and a new Native-branch assertion
guards against the ~54k-token schema duplication the original PR
fixed.

## `composio/ops.rs` — don't cache degraded snapshots

All three backend calls in `fetch_connected_integrations_uncached`
(`list_toolkits`, `list_connections`, `list_tools`) previously
returned `Some(Vec::new())` or fell through to an empty inner vec
on transient errors. The outer `fetch_connected_integrations`
caches whatever the uncached path returns, so a single transient
5xx would silently hide every integration, mark connected toolkits
as disconnected, or register dynamic Composio tools with zero
callable actions — until the cache is invalidated or the process
restarts. Changed all three branches to return `None`, which
signals the caller to NOT cache the result and retry on the next
call.

## `composio/ops.rs` — prefix match needs a delimiter

`starts_with(&slug.to_uppercase())` false-matches when two toolkit
slugs share a text prefix (e.g. `git` vs `github`). The current
allowlist doesn't trigger this, but adding a new toolkit could
silently leak actions between integrations. Anchored the prefix
with an underscore so `GMAIL_SEND_EMAIL` matches `gmail_`, not
just `gmail`.

## `scripts/stage-core-sidecar.mjs` — resolve CARGO_TARGET_DIR vs repo root

`resolve(process.env.CARGO_TARGET_DIR)` uses the process's current
working directory, but the `cargo build` spawn below runs with
`cwd: root`. For an absolute env var this is identical, but a
relative `CARGO_TARGET_DIR` and a cwd outside the repo would make
the two paths disagree and the binary lookup would miss. Defensive
fix: `resolve(root, process.env.CARGO_TARGET_DIR)` so both paths
anchor to the same base.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:07:26 -07:00
1836c7691f Feat: smart routing (#569)
* chore: update OpenHuman version to 0.52.9 and add intelligent routing functionality

- Bumped OpenHuman version from 0.52.7 to 0.52.9 in Cargo.lock files.
- Introduced a new routing module that implements intelligent model routing based on task complexity and local model health.
- Added a health checker for the local Ollama model server to improve routing decisions.
- Enhanced the provider to classify tasks and determine the appropriate backend (local or remote) for processing requests.
- Updated related files to support the new routing logic and ensure seamless integration with existing functionalities.

* refactor(routing): streamline provider and enhance routing logic

- Removed the `build_tool_instructions` function from the public API, simplifying the routing module.
- Updated the `IntelligentRoutingProvider` to utilize a more efficient model resolution process, improving routing decisions based on task complexity and local model health.
- Introduced a new `quality` module to assess response quality, enabling better fallback decisions when local responses are deemed low quality.
- Enhanced the `RoutingHints` struct to provide more granular control over routing behavior, including privacy requirements and cost sensitivity.
- Added tests to validate the new routing logic and quality assessment, ensuring robust functionality across various scenarios.

* feat(tests): add live end-to-end routing tests for real backend integration

- Introduced a new test file `live_routing_e2e.rs` containing end-to-end tests for routing against a live backend.
- Tests require a valid backend URL, user session JWT, and real network interactions, hence marked as `#[ignore]`.
- Implemented functionality to set up environment variables, write configuration files, and perform JSON-RPC calls to validate routing behavior.
- Added assertions to ensure correct handling of various routing cases, enhancing test coverage for the routing module.

* refactor(format): ran format command

* feat(routing): add IntelligentRoutingProvider and enhance LocalHealthChecker

- Introduced a new `factory.rs` file containing the `new_provider` function to construct an `IntelligentRoutingProvider` that integrates local AI capabilities with remote backend providers.
- Enhanced the `LocalHealthChecker` in `health.rs` by adding a `reqwest::Client` for improved health probing, including better logging for cache hits and misses, and streamlined cache updates.
- Updated health check logic to utilize the new client, ensuring more reliable health status checks for local AI services.

* refactor(routing): move new_provider function to factory module

- Moved the `new_provider` function from `mod.rs` to a new `factory.rs` module to improve code organization and maintainability.
- Updated public exports to include the new location of `new_provider`, ensuring continued accessibility for constructing `IntelligentRoutingProvider` instances.
- Removed the old implementation from `mod.rs`, streamlining the routing module's structure.

* refactor(routing): simplify local task routing logic

- Removed redundant conditions for routing medium tasks locally, streamlining the decision-making process in the `decide` function.
- Updated comments to reflect the simplified logic, enhancing code clarity and maintainability.

* docs(tests): clarify comments in json_rpc_e2e.rs regarding hint overrides
logic.

* refactor(tests): enhance live routing end-to-end tests with timeout handling

- Introduced a timeout mechanism for reading SSE events to prevent indefinite blocking.
- Updated environment variable management in tests to ensure safe access and cleanup.
- Improved comments for clarity regarding the safety of environment variable mutations during tests.

* refactor(tests): update SSE event reading in live routing tests.

* refactor(routing): enhance medium task routing logic and update comments

- Updated the routing logic for medium tasks to utilize hints for local bias, ensuring more accurate routing decisions.
- Revised comments throughout the code to clarify the behavior of task categories and routing preferences.
- Adjusted test cases to reflect the new routing logic, ensuring they accurately validate the expected behavior for medium tasks.

* refactor(tests): implement timeout handling for dictation event reception

- Added a timeout mechanism to the dictation event test to prevent indefinite blocking while waiting for the "pressed" event.
- Enhanced the test logic to consume events until the expected event type is received, improving reliability and clarity in the test flow.

---------

Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
2026-04-14 13:06:48 -07:00
b2c74458d3 fix(voice): enable GPU detection and Metal acceleration for whisper (#558) (#571)
* fix(device): expand GPU detection with Intel Mac and NVIDIA probes (#558)

Add Intel Mac detection (no Metal GPU for whisper), nvidia-smi probe
for Windows/Linux NVIDIA GPUs, and diagnostic tracing at each
decision point in detect_gpu().

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

* build(cargo): enable whisper-rs Metal feature on macOS (#558)

Add target-specific dependency for macOS that enables the `metal`
feature on whisper-rs, compiling whisper.cpp with Metal GPU support.
Cargo merges features from both declarations so non-macOS builds
are unaffected.

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

* fix(whisper): configure GPU params from device profile (#558)

Accept has_gpu and gpu_description in load_engine() and explicitly
set use_gpu and flash_attn on WhisperContextParameters instead of
relying on the compile-time default. Log the selected acceleration
backend at startup.

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

* fix(local_ai): pass GPU info to whisper engine load paths (#558)

Thread DeviceProfile has_gpu and gpu_description through both the
bootstrap (startup) and speech (lazy) whisper engine load calls so
the engine can configure Metal or CUDA acceleration at runtime.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 13:06:08 -07:00
CodeGhost21andGitHub 13ce2cdcbf test(coverage): raise Rust unit test coverage to ≥80% on 10 critical modules (issue #530) (#567)
* test(coverage): raise 9 critical modules to ≥80% line coverage

Pushes unit test coverage to meet issue #530's 80% target on 9 modules:
  socket (17% → 80%), credentials (46% → 81%), composio (55% → 81%),
  memory (75% → 80%), tools (73% → 83%), plus previously-passing
  cron, context, learning, embeddings.

Additions span ~400+ new tests across 44 files:
  - Pure-function tests for types, schemas, parsers, URL builders,
    and error-classification helpers.
  - Mock-backend integration tests using axum for HTTP-dependent
    code (composio client + ops, socket ws_loop against a local
    WebSocket server).
  - End-to-end RPC handler tests using tempdir + OPENHUMAN_WORKSPACE
    env override (credentials, memory, config, cron).

Also fixes 4 pre-existing flaky tests on macOS dev machines by
pinning absolute paths (/bin/ls, /bin/sleep, /bin/sh, /usr/bin/touch)
in cron scheduler tests so sh -lc does not pick up homebrew-shadowed
binaries that macOS SIP refuses to execute, and relaxing a
screen_intelligence capture-permission hang assertion.

Remaining work: voice, config, screen_intelligence, channels,
local_ai still below 80% (62%, 61%, 54%, 68%, 35% respectively).

* test(coverage): add config module tests (62% → ~71%)

Adds tests for:
- config/ops.rs: env_flag_enabled, core_rpc_url_from_env, snapshot_config_json,
  workspace_onboarding_flag_exists/_set, set_browser_allow_all, agent_server_status
- config/schemas.rs: catalog parity, all 21 registered schema keys, helpers
- config/schema/proxy.rs: normalize_*, parse_proxy_scope, parse_proxy_enabled,
  validate_proxy_url, service_selector_matches, ProxyConfig defaults
- config/schema/load.rs: apply_env_overrides for api_key, model, temperature,
  reasoning_enabled, web_search.*, storage.*; resolve_config_dir_for_workspace
- config/settings_cli.rs: settings_section_json all sections (model/memory/runtime/browser/unknown)

* test(coverage): config reaches 80.49%, socket back to 80.10%

- config/ops.rs: apply_model/memory/runtime/browser/analytics/screen_intelligence_settings
  roundtrips via in-memory Config; load_and_apply_dictation/voice_server_settings
  activation-mode validation; get_dictation/voice_server/onboarding readers;
  workspace_onboarding_flag_set/resolve error and happy paths.
- config/schema/load.rs: apply_env_overrides coverage for OPENHUMAN_MODEL,
  OPENHUMAN_TEMPERATURE (range clamp), OPENHUMAN_REASONING_ENABLED,
  OPENHUMAN_WEB_SEARCH_*, OPENHUMAN_STORAGE_* env branches.
- config/schema/proxy.rs: normalize_* helpers, parse_proxy_scope/enabled,
  ProxyConfig defaults, validate_proxy_url schemes + hosts,
  service_selector_matches wildcards.
- config/schemas.rs: every registered controller key, namespace parity.
- config/settings_cli.rs: all section projections (model/memory/runtime/browser/unknown).
- Shared `TEST_ENV_LOCK` at config::mod level so config::ops and config::schema::load
  test modules serialize OPENHUMAN_WORKSPACE mutations.
- socket: a couple more schema assertions to keep module at 80%+.

* test(coverage): channels partial push (68.12% → 68.91%)

- channels/controllers/schemas.rs: every registered key resolves, required
  input coverage for describe/send_message/telegram_login_check.
- channels/providers/web.rs: catalog parity, chat/cancel schema required
  inputs, unknown fallback, key_for, event_session_id_for stability,
  normalize_model_override trimming/empty, broadcast channel subscribe,
  field-builder helpers.
- channels/providers/discord/api.rs: auth_header prefix, BotPermissionCheck
  serde with empty/full missing_permissions lists, permission bit flags
  are single-bit and distinct.

Channels remains below the 80% issue target — the bulk of remaining
uncovered code lives in telegram/channel.rs (574 lines), ops.rs (462),
lark.rs (433) and runtime/startup.rs (350), which depend on live HTTP /
socket / runtime bootstrap state that would require extensive mock
infrastructure to exercise from unit tests.

* test(coverage): partial push on local_ai (34→45%), screen_intelligence (54→62%), voice (62→63%)

- local_ai/schemas.rs: catalog parity, every registered key resolves,
  field-builder helpers, deserialize_params happy/error, download_force optional.
- local_ai/ops.rs: local-ai-disabled error paths for prompt/vision_prompt/
  embed/summarize/transcribe/tts/chat; empty-messages rejection; suggestions
  return empty when disabled (graceful degradation).
- local_ai/install.rs: find_system_ollama_binary env-override happy/missing/
  empty cases; PATH-based lookup stub.
- screen_intelligence/schemas.rs: catalog parity, all 15 registered keys
  resolve to non-unknown, unknown fallback.
- screen_intelligence/ops.rs: accessibility_status/doctor_cli_json/
  capture_image_ref/stop_session/vision_recent error-free behaviour.
- voice/server.rs: truncate_for_log ellipsis + multibyte; try_global_server
  after init; additional hallucination-pattern coverage.

Remaining gap on these modules lives almost entirely in:
- local_ai/service/ollama_admin.rs (625 lines) — real HTTP + Ollama subprocess
- local_ai/service/{assets,public_infer,vision_embed}.rs — same
- voice/{server,audio_capture}.rs deep paths — audio hardware
- screen_intelligence/{engine,processing_worker}.rs — active capture session

* test(coverage): channels providers (+0.9%) — qq ensure_https, lark parsers

- qq: ensure_https accept/reject, QQ_API_BASE/AUTH_URL constants, constructor.
- lark: parse_post_content zh_cn/en_us fallback + links/mentions; invalid
  JSON returns None; strip_at_placeholders for @_user_N tokens; group
  should_respond_in_group mention gating.

* test(coverage): push all 4 remaining modules

- discord/api.rs: list_bot_guilds_at_base/list_guild_channels_at_base test
  seams with mock axum server; parse happy-path, error status, channel
  filter+sort, empty list.
- channels/controllers/ops.rs: parse_allowed_users for string CSV/array/
  newline/@-prefix/case-insensitive dedup/non-string; credential_provider;
  list_channels/describe_channel; connect_channel unknown-channel and
  non-object credentials.
- local_ai/ollama_api.rs: `ollama_base_url()` honours OPENHUMAN_OLLAMA_BASE_URL
  env var so tests can point at mock servers; DEFAULT_OLLAMA_BASE_URL
  preserved.
- local_ai/service/public_infer.rs: mock-backend tests for inference/prompt
  happy path, non-success status, suggest_questions parsing, disabled-
  local-ai short-circuits for summarize/prompt/suggest_questions/
  inline_complete.
- voice/schemas.rs: overlay_notify cancelled→released, unknown state
  errors, missing state errors, server_start handler, TranscribeParams +
  TtsParams deserialize happy/error paths, server_start all-optional
  invariant, description completeness.

* test(coverage): local_ai HTTP mock tests (49→53%)

- ollama_api: ollama_base_url() helper now used by ollama_admin/has_model
  + ollama_healthy (in addition to public_infer + vision_embed).
- public_infer: inference against mock /api/generate happy/error/empty;
  suggest_questions parses line-separated output; disabled short-circuits
  for summarize/prompt/suggest/inline_complete.
- vision_embed: mock /api/embed with /api/tags preflight; empty-input
  rejection; disabled short-circuits for embed and vision_prompt.
- ollama_admin: has_model matches exact + prefixed tags; errors on 5xx
  /api/tags; ollama_healthy true on 200 and false on unreachable URL.

* test(coverage): more local_ai + voice gains

- local_ai/ollama_admin: diagnostics against mock Ollama (unreachable +
  missing models + all models present), list_models happy/error paths.
- voice/dictation_listener: start_if_enabled early-returns for disabled/
  empty-hotkey/unparseable-hotkey; normalize_hotkey_for_rdev coverage
  for Shift+Alt, lowercase, function keys, whitespace trimming.

* test(coverage): local_ai schemas handlers + voice flakiness fix

- local_ai/schemas: handle_device_profile; handle_presets tier+device
  shape; handle_apply_preset invalid/custom/valid paths; handle_set_ollama_path
  nonexistent/empty-to-clear paths.
- voice/schemas: relax server_status/stop assertion to tolerate other
  tests in the same binary having initialised the global voice server
  (it's a OnceLock, so state is shared across the whole test process).

* test(coverage): channels incremental push (71→73%)

- presentation.rs: split_sentences, group_sentences, merge_short,
  segment_delay monotonic/bounded, is_structured_content detection,
  segment_for_delivery edge cases.
- runtime/dispatch.rs: contains_any, starts_with_any, full coverage of
  select_acknowledgment_reaction across all 7 categories + deterministic
  + empty/single-char inputs.
- commands.rs: doctor_channels with telegram/discord/slack/imessage/
  multiple-config branches.
- discord/api: check_channel_permissions mock-server tests — admin bypass,
  all-missing, everyone-allow, channel overwrite deny, member lookup
  failure. Added check_channel_permissions_at_base seam.
- lark: should_refresh_last_recv, LarkChannel::new, is_user_allowed
  wildcard/empty allowlist, parse_event_payload edge cases (unsupported
  type / empty sender / missing event / post type).
- email_channel: is_sender_allowed full matrix (empty/wildcard/exact/
  @-prefix/bare-domain/subdomain-confusion), strip_html empty/tags-only/
  unclosed/whitespace collapse.
- voice/schemas: tolerate event interleaving in broadcast-channel test
  (schema bus is process-global).

* test(coverage): address review findings — assertions, determinism, observability

Tighten assertions so regressions surface:
- credentials/ops: assert decrypt migrate path returns Ok
- credentials/session_support: assert trimmed profile name directly
- computer/mouse: check Err branch in single-axis scroll tests
- filesystem/git_operations: assert exact error substrings and verify
  `git init` success before each test
- channels/controllers/ops: exact credential_provider key + concrete
  parse_allowed_users expectation with accurate normalisation note;
  merged the three duplicate list/describe tests into existing coverage
- tools/impl/browser/screenshot: cfg-branched support-matrix assertions
- tools/impl/agent: replace misleading stub test with one that actually
  exercises dispatch_subagent's graceful-failure paths
- local_ai/install: cfg-branched build_install_command expectations
- local_ai/service/public_infer: exercise empty-response reject path via
  inference() (allow_empty=false) and assert the error
- screen_intelligence: only take the macOS slow-path skip on macOS

Test determinism:
- local_ai/install: serialise env mutations via a module Mutex and RAII
  EnvGuard that restores prior values on drop
- composio/ops: poll TCP readiness with backoff before returning the
  mock-backend URL
- memory/global: bind TempDir at test scope so its workspace outlives
  any lazy-init reference

Consistency:
- local_ai/service/ollama_admin: route every Ollama HTTP call and the
  diagnostics URL through ollama_base_url(); drop the stale
  OLLAMA_BASE_URL import so /api/pull, /api/tags (runner check),
  /api/show, and the health message all honour the env override

Observability:
- local_ai/service/vision_embed: tracing::debug at entry/response and
  tracing::error on send/non-success
- local_ai/service/ollama_admin::list_models: entry/response/parse
  logging including raw body on parse failure
- channels/providers/discord/api: tracing::debug with endpoint, status,
  body, and context before every non-success bail!

New coverage:
- channels/providers/lark: anchor href-only fallback in parse_post_content
- local_ai/ollama_api: five-case env-override suite for ollama_base_url
  (unset / normal / trimmed / trailing slashes / empty|whitespace)

Miscellaneous:
- voice/server: OnceCell-backed comment (was OnceLock); drop duplicate
  initial-status test; supply the HallucinationMode argument to two
  stale hallucination tests so the crate type-checks
2026-04-14 12:52:59 -07:00
Steven EnamakelandGitHub 7ae2bf83b2 feat: disable permission auto-prompts + typing indicator on webhook channels (#552)
* feat(config): default screen intelligence, dictation, and vision model off

Flip defaults so no macOS TCC permission prompt fires on first run:

- `dictation.enabled`: `true` → `false` (was auto-starting rdev::listen,
  which requests Accessibility/Input Monitoring on macOS)
- `screen_intelligence.use_vision_model`: `true` → `false` (fewer
  surprise vision-model calls; Pass 1 Apple Vision OCR still runs)

Aligns all permission-gated auto-starts on a consistent opt-in posture:
`screen_intelligence.enabled`, `autocomplete.enabled`, and
`voice_server.auto_start` already default to `false`. Users must now
explicitly flip each toggle (config or JSON-RPC) before the core triggers
any OS permission dialog.

* feat(channels): fire typing indicator on webhook-inbound path

Two inbound flows exist today and only one fires typing:

- Local bot (`channels_config.telegram.bot_token` set) → dispatch.rs
  already calls `channel.start_typing` + `spawn_scoped_typing_task`
- Backend webhook (Telegram → backend → socket.io → core) →
  `ChannelInboundSubscriber` had **no typing call** — replies route via
  backend REST, so the local `Channel` trait isn't reachable.

Close the gap by going through the backend:

- `api/rest.rs`: add `send_channel_typing(channel, jwt, body)` hitting
  the new `POST /channels/:id/typing` backend route.
- `channels/bus.rs`: extract the agent-wait loop into `run_agent_loop`
  and wrap it with a typing task that fires immediately on `start_chat`
  success, refreshes every 4s (beats Telegram's ~5s and Discord's ~10s
  typing TTLs), and cancels on every exit path (done / error / empty /
  bus-closed / lagged / timeout). Backend failures log at debug — a
  flaky typing call must never block the reply flow.

Generalises to every channel with a backend adapter; adapters without a
native typing API no-op gracefully.

* Enhance test stability by introducing a Mutex guard for TRIAGE_DISABLED_ENV in tests

- Added a static Mutex guard to ensure safe concurrent access to the `TRIAGE_DISABLED_ENV` variable during tests, preventing interleaved set_var/remove_var calls that could lead to spurious failures.
- Updated relevant test cases to acquire the Mutex lock when accessing the environment variable, ensuring consistent behavior across concurrent test executions.
2026-04-14 08:19:57 -07:00
933c233704 fix(voice): add hallucination filter to chat voice path (#553) (#556)
* fix(voice): add hallucination filter to chat voice path and improve detector (#553)

The chat voice transcription pipeline (ops.rs voice_transcribe_bytes) had
no hallucination filtering, unlike the desktop dictation server which has
had it since inception. This caused Whisper to inject repetitive garbage
text ("it... it... it...", "Thank you. Thank you. Thank you.") into chat
voice input, especially on short/silent recordings.

Changes:
- Extract hallucination detection into shared voice/hallucination.rs module
- Add hallucination filter to voice_transcribe_bytes (chat voice path)
- Improve detector: strip punctuation before word comparison, add
  dominant-word ratio check (>40%), catch "it... it... it..." patterns
- Add 14 unit tests covering exact match, repetition, ratio, and
  legitimate speech cases

Closes #553

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

* refactor(voice): introduce HallucinationMode enum with split pattern lists

Split monolithic HALLUCINATION_PATTERNS into ALWAYS_HALLUCINATION
(blank-audio, YouTube phrases — filtered in all modes) and
DICTATION_ONLY_PATTERNS (single-word noise like "yes", "okay" —
filtered only in desktop dictation). Add repeating n-gram detection
for looping phrases ("Thank you. Thank you. Thank you.") and raise
dominant-word ratio from >40%/3 to >60%/5 to prevent false positives
on emphatic speech like "no no no don't do that".

Addresses CodeRabbit review on PR #556.

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

* fix(voice): use Conversation mode for chat, Dictation mode for desktop

Wire HallucinationMode into both voice paths:
- ops.rs (chat voice): HallucinationMode::Conversation — conservative
  filtering allows short replies like "yes", "okay", "thank you"
- server.rs (desktop dictation): HallucinationMode::Dictation —
  aggressive filtering drops single-word noise artifacts
- Update server.rs hallucination_detection test for new signature

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

* fix(voice): redact user transcript content from debug logs

Remove raw text interpolation (normalized, first, word, pattern) from
hallucination detection debug logs to prevent leaking sensitive speech
content. Retain non-PII metadata (repeat counts, ratios, n-gram length)
for diagnostics.

Addresses CodeRabbit review on PR #556.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 07:33:33 -07:00
YellowSnnowmannandGitHub 219a2ff9af Fix: GitHub composeio (#543)
* feat(composio): add GitHub repository management and trigger creation

- Enhanced the ComposioConnectModal component to support loading and selecting GitHub repositories for connected users.
- Introduced new API functions for listing GitHub repositories and creating triggers, improving integration capabilities with GitHub.
- Added state management for repository loading, selection, and trigger action feedback, enhancing user experience during GitHub integration.
- Updated types to include GitHub repository structures and responses, ensuring type safety and clarity in the codebase.

* feat(composio): add GitHub repository listing and trigger creation APIs

- Implemented new API endpoints for listing GitHub repositories and creating triggers, enhancing integration capabilities with GitHub.
- Updated types to include responses for GitHub repositories and trigger creation, ensuring type safety and clarity.
- Added corresponding handler functions and schemas to support the new operations, improving overall functionality and user experience in the Composio integration.

* refactor(composio): remove GitHub repository and trigger management from ComposioConnectModal

- Eliminated GitHub repository loading and trigger creation logic from the ComposioConnectModal component, streamlining its functionality.
- Removed associated state management and API calls for GitHub repositories and triggers, enhancing code clarity and maintainability.
- Updated types to reflect the removal of GitHub-related structures, ensuring consistency across the codebase.

* format: ran format
2026-04-14 15:58:12 +05:30
Steven EnamakelandGitHub 2824850708 feat(streaming): thinking tokens, tool call deltas, and progressive channel edits (#549)
* feat(conversations): implement live streaming for assistant responses

- Added support for live streaming of assistant responses in the Conversations component, enhancing user experience during interactions.
- Introduced new interfaces for handling streaming state, including `StreamingAssistantState` and related event handlers for text, thinking, and tool argument deltas.
- Updated the state management to accommodate streaming data, ensuring smooth updates to the UI as new content arrives.
- Enhanced the rendering logic to display provisional assistant bubbles and thinking indicators while responses are being composed.
- Refactored existing event handling to integrate with the new streaming functionality, improving overall responsiveness and interactivity.

This commit significantly improves the real-time interaction capabilities of the assistant, providing users with immediate feedback during conversations.

* feat(channels): progressive-edit streaming for inbound channels

ChannelInboundSubscriber now buffers text/tool delta events on a 1s
timer and edits the outbound channel message in place, so Telegram/
Slack-style conduits can show a single evolving reply instead of a
single atomic bubble at the end. Renders a "🔧 tool …" status line
above the partial text and rewrites with the final canonical reply
on chat_done.

Falls back to atomic-final delivery if the backend's PATCH
/channels/:channel/messages/:id endpoint is unavailable or repeatedly
fails, so existing channels keep working while the edit endpoint is
rolled out.

New rest.rs helper: BackendOAuthClient::send_channel_edit.

* style: cargo fmt on streaming-related files

Auto-applied by cargo fmt after the streaming plumbing changes touched
provider traits, the session turn loop, the channel inbound subscriber,
and the compatible-provider SSE path.

* feat(streaming): compact live preview + Telegram typing indicator

UI: while streaming, show only the trailing ~120 chars of assistant
text in a monospace ticker-tape bubble (cursor + ellipsis prefix) so
the scroll position doesn't jump as tokens arrive. The full response
still replaces the preview via addInferenceResponse on chat_done.

Backend bridge: ChannelInboundSubscriber now fires Telegram's typing
indicator as soon as an inbound message is received and refreshes it
every 4 s while the turn is in flight (Telegram's sendChatAction lasts
~5 s). New BackendOAuthClient::send_channel_typing hits
POST /channels/:channel/typing; the subscriber latches
typing_disabled after two failures so channels without the endpoint
stop getting hit.

* fix(providers): fall back to JSON parse when upstream ignores stream=true

Some OpenAI-compatible backends (including our e2e mock) accept
`stream: true` in the request but reply with a regular
`application/json` body rather than an SSE stream. The previous
implementation blindly pushed the body through the SSE line parser,
which produced an empty aggregated response because the body never
contained `\n\n` event separators.

Detect the non-SSE content-type and fall through to the existing
`parse_native_response` path so the caller still gets the aggregated
response. No deltas are emitted in this case — there's nothing to
stream — but correctness is preserved.

Unblocks the json_rpc_protocol_auth_and_agent_hello E2E test.

* Enhance event handling for progressive-edit streaming in ChannelInboundSubscriber

- Introduced a new `StreamingState` struct to manage the state of progressive-edit streaming, allowing for buffered text and tool deltas.
- Implemented a timer-based flushing mechanism for edits, ensuring timely updates to the channel.
- Refactored event handling logic to accommodate new event types (`text_delta`, `tool_call`, `tool_result`) and improved error handling for chat events.
- Updated the logic for finalizing channel replies to handle both streaming and atomic delivery scenarios, enhancing overall responsiveness and user experience.

This commit significantly improves the handling of real-time updates during user interactions, providing a smoother and more interactive experience.

* Enhance tool call tracking in AgentProgress and parsing logic

- Added a `call_id` field to `ToolCallStarted` and `ToolCallCompleted` variants in the `AgentProgress` enum to uniquely identify tool calls and link them to their respective events.
- Updated the `ParsedToolCall` struct to include an optional `id` field for tool calls originating from native responses, ensuring consistent tracking across different call types.
- Modified the `parse_tool_call_value` function to initialize the `id` field appropriately, enhancing the parsing logic for tool calls.
- Adjusted the `run_tool_call_loop` function to utilize the new `call_id` for progress events, ensuring accurate tracking of tool execution across iterations.
- Enhanced the `spawn_progress_bridge` function to handle the new `call_id` field in progress events, improving the overall event handling mechanism.

These changes improve the robustness of tool call tracking and enhance the clarity of event relationships within the agent's progress reporting.

* Enhance tool call tracking and event handling in Conversations component

- Added a `tool_call_id` field to `ChatToolCallEvent` and `ChatToolResultEvent` interfaces for improved tracking of tool calls across events.
- Updated event key generation in the Conversations component to include `tool_call_id`, ensuring unique identification of tool events.
- Enhanced the logic for managing tool timelines to prevent duplication of entries by reconciling existing tool calls based on `tool_call_id`.
- Improved handling of tool argument deltas and results, allowing for more accurate updates to the UI during tool execution.

These changes significantly improve the robustness of tool call tracking and enhance the clarity of event relationships within the Conversations component.

* Enhance tool call event handling and progress reporting in Agent

- Updated the `run_tool_call_loop` function to include stable IDs for tool calls, ensuring unique identification across iterations.
- Introduced early completion events for failed tool calls, improving client-side error handling and user experience.
- Refactored the `emit_progress` function to use asynchronous sending, ensuring lifecycle events are not dropped due to backpressure.
- Enhanced the `turn` method to await progress emissions, improving synchronization during user message processing.

These changes significantly improve the robustness of tool call tracking and enhance the clarity of event relationships within the agent's progress reporting.

* fix(channels): close stuck drafts, prevent duplicate post, add bridge logs

Findings #3, #5, #6 from the follow-up review:

channels/bus.rs
- chat_done handler no longer returns early on empty reply — it now
  finalizes with a "(No response from agent.)" fallback so any draft
  we posted during streaming gets closed off instead of being left
  showing "_working…_" forever.
- StreamingState gained `draft_sent: bool`, set whenever the initial
  send_channel_message succeeds (even when the backend's response
  didn't include an id). finalize_channel_reply now checks this flag
  and silently skips the "no draft → send atomic" fallback when a
  draft was posted but id was lost — fixes a duplicate-bubble bug
  where an id-less draft plus a chat_done finalize produced two
  messages in the user's channel.

channels/providers/web.rs
- spawn_progress_bridge now logs a scoped entry message on startup
  (client_id/thread_id/request_id), a per-variant trace/debug line on
  each AgentProgress event (with call_id/iteration correlation), and
  an exit message with final round + events_seen count. SubagentFailed
  is logged at warn level for visibility.
2026-04-13 20:38:00 -07:00
Steven EnamakelandGitHub 3db06e8e92 feat(prompt): per-agent PROFILE.md + MEMORY.md injection, 2K-char cap (#550)
* Enhance debug-agent-prompts script and prompt rendering logic

- Updated the `debug-agent-prompts.sh` script to always run `cargo build`, ensuring the latest binary is used and preventing stale binaries from affecting agent behavior.
- Modified the output directory handling to wipe and recreate it at the start of each run, ensuring a clean snapshot of the current agent set.
- Enhanced the `render_subagent_system_prompt` function to unconditionally inject `PROFILE.md`, ensuring it is included even when identity information is omitted, thus improving personalization for agents like `welcome`.
- Added tests to verify the correct injection of `PROFILE.md` under various conditions, ensuring robust functionality and preventing regressions in prompt rendering.

* Enhance debug-agent-prompts script to utilize the currently-logged-in user's workspace

- Updated the `debug-agent-prompts.sh` script to point to the real user's workspace, ensuring onboarding-generated files like `PROFILE.md` are included in the dump.
- Improved workspace resolution logic to prioritize the active user's workspace, falling back to default paths as necessary.
- Added error handling for cases where the workspace is not found, providing clear guidance for users to complete onboarding or specify a different workspace.
- Enhanced output to include the presence state of `PROFILE.md`, improving visibility into the onboarding process.

* Add profile inclusion for user-facing agents to enhance personalization

- Updated agent TOML configurations for orchestrator, trigger reactor, trigger triage, and welcome agents to include user profile data by setting `omit_profile = false`. This allows agents to personalize interactions based on user context derived from `PROFILE.md`.
- Refactored the `AgentDefinition` struct to include a new `omit_profile` field, ensuring that agents can opt-in to utilize user profile information.
- Enhanced prompt rendering logic to conditionally inject `PROFILE.md` based on the `omit_profile` flag, improving the relevance and personalization of agent responses.
- Updated tests to verify the correct behavior of profile inclusion across various agents, ensuring robust functionality and preventing regressions.

* Implement `omit_profile` flag in `AgentBuilder` and `Agent` for profile management

- Added a new `omit_profile` field to the `AgentBuilder` struct, allowing agents to specify whether to include user profile data in their responses.
- Updated the `Agent` struct to mirror the `omit_profile` flag, ensuring that the profile inclusion logic is consistent across agent instances.
- Enhanced the `omit_profile` method in `AgentBuilder` to facilitate the configuration of this flag during agent construction.
- Adjusted the default behavior to omit profiles for legacy agents while allowing opt-in for specific agents that require user context.
- Updated documentation to clarify the purpose and usage of the `omit_profile` flag in agent definitions.

* Implement profile management enhancements in Agent and prompt rendering

- Introduced an `omit_profile` flag in the `AgentBuilder` and `Agent` to control the inclusion of user profile data in responses, defaulting to true for legacy paths.
- Updated the `Agent` struct to utilize the `omit_profile` flag, ensuring consistent profile inclusion logic across agent instances.
- Enhanced prompt rendering logic to conditionally include or exclude `PROFILE.md` based on the `omit_profile` setting, improving personalization for user-facing agents.
- Added tests to verify the correct behavior of profile inclusion and omission across various scenarios, ensuring robust functionality and preventing regressions.

* feat(prompt): per-agent MEMORY.md injection with 2000-char cap

Add `omit_memory_md` to `AgentDefinition` (mirror of `omit_profile`)
and inject `MEMORY.md` alongside `PROFILE.md` in both the main and
sub-agent render paths. Both user-specific files are capped at
`USER_FILE_MAX_CHARS = 2_000` (~1000 tokens each) via a new
`inject_workspace_file_capped` helper so growing on-disk files can't
balloon the system prompt.

Opt-in on the same four user-facing agents (welcome, orchestrator,
trigger_triage, trigger_reactor). Narrow specialists leave it at the
`true` default.

KV-cache contract is documented on the flag, the injection sites, and
the capped helper: rendered bytes are frozen per session, and
mid-session writes only surface on the next session. Pinned with a new
`rendered_subagent_system_prompt_is_byte_stable_across_repeat_calls`
test plus coverage for injection / opt-out / 2000-char cap.

* refactor(tests): clean up test formatting and improve readability

- Removed unnecessary line breaks and adjusted indentation in test files for better consistency and clarity.
- Reformatted the `RewardsCouponSection` test to enhance readability and maintain a uniform style across test cases.
- Ensured that all test cases align with the updated formatting standards, improving overall maintainability.

* feat(debug-agent-prompts): enhance output directory validation and canonicalization

- Improved the `debug-agent-prompts.sh` script to validate and canonicalize the output directory (`OUT_DIR`) before performing any file operations.
- Added checks to reject relative paths and ensure the output directory is an absolute path, preventing potential catastrophic deletions.
- Implemented a `canonicalize` function that uses `realpath` or `readlink` to resolve paths, with a fallback to Python for compatibility on barebones systems.
- Enhanced error handling to provide clear feedback when the output directory cannot be validated or canonicalized.
- Ensured that the script operates on the canonicalized path for all subsequent commands, maintaining consistency and safety.

* style(turn): single-line rustfmt for redacted log branch
2026-04-13 20:06:00 -07:00
Steven EnamakelandGitHub 8cbb425cea feat(onboarding): fire welcome agent immediately on completion (#548)
* Implement proactive welcome agent for onboarding experience

- Added a new module `welcome_proactive` to handle immediate welcome messages after user onboarding completion, enhancing user engagement.
- Updated `set_onboarding_completed` to trigger the proactive welcome agent upon onboarding status change, ensuring timely delivery of welcome messages.
- Refactored the onboarding process to streamline the integration of proactive messaging, improving overall user experience.
- Enhanced documentation and logging for better observability of the onboarding flow and proactive message handling.
- Introduced tests to validate the functionality of the new welcome agent and its integration into the onboarding process.

* fix(socketio): auto-join "system" room so proactive messages reach clients

The proactive message subscriber emits with `client_id="system"`, but
connected Socket.IO clients only auto-joined a room named after their
own sid — so the welcome agent's message was published into an empty
room and never reached any frontend.

Adds `socket.join("system")` at connect time alongside the existing
per-sid join, so every client now receives broadcast proactive events
(welcome agent, morning briefing, cron announcements).

Also adds scripts/test-proactive-welcome.sh — end-to-end smoke test
that resets onboarding flags, spawns a fresh core on port 7789,
connects a Socket.IO listener, fires the RPC, and reports pass/miss
for every checkpoint in the pipeline including client-side delivery.

* fix(welcome): PR review — tighter gating, legacy-cron prune, safer harness

- Gate proactive welcome spawn on `!was_chat_completed` so a user
  whose chat flow already completed (legacy tool path or manual flip)
  doesn't get a second welcome.
- Prune legacy `welcome` cron jobs in `seed_proactive_agents`: one-
  shot entries left behind by an interrupted earlier run would
  otherwise double-deliver once the scheduler picks them up. Added
  a unit test covering the prune + morning-briefing seed in the same
  call.
- Add a post-publish debug log in `welcome_proactive::run_proactive_welcome`
  so "reached end successfully" is distinguishable from "silently
  bailed above" by reading the log alone.
- Replace ignored `socket.join(...)` results with a helper that logs
  success + failure per room + client id. Silent join failures on the
  "system" room make proactive delivery vanish without a trace.
- Harden the test harness:
  * Back up CONFIG_PATH before mutating and restore on exit so the
    developer's staging profile is never permanently changed (unless
    `--keep-flags` is passed).
  * Tolerate inline comments + trailing whitespace in the TOML
    rewrite; append-at-end instead of prepend when a key is missing,
    so the rewrite can't accidentally land inside a section header.

Not applied:
- Unit tests for `run_proactive_welcome` empty-response / serialization-
  failure paths: empty response is already `anyhow::bail!`-guarded, and
  the serde_json failure branch is unreachable given a `json!`-built
  value. Testing either requires mocking `Agent::from_config_for_agent`
  which has heavy registry/provider dependencies — cost > value.
2026-04-13 19:43:24 -07:00
Steven EnamakelandGitHub 5a8a7edb91 Refine billing, settings, rewards, and usage UI (#547)
* feat: add react-icons support and refactor skill category handling

- Added `react-icons` dependency to the project for enhanced icon usage.
- Introduced new skill icons in the `toolkitMeta.tsx` component, replacing SVG icons with `react-icons` for improved maintainability and consistency.
- Created a new `skillCategories.ts` file to define skill categories and their order, streamlining the management of skill categories across the application.
- Refactored the `SkillCategoryFilter` component to utilize the new skill categories structure, enhancing clarity and reducing redundancy in the codebase.
- Updated the `Skills` page to leverage the new icon rendering method and skill categories, improving the overall user experience.
- Added tests to ensure the correct functionality of the new skill category and icon implementations.

* feat: update environment configuration and enhance settings UI

- Updated `.env.example` and `app/.env.example` to reflect new backend URL and added optional environment selector for staging.
- Enhanced `SettingsHome` component by separating account and billing sections for improved clarity.
- Introduced a new billing section in the settings menu to streamline user navigation.
- Updated `useSettingsNavigation` hook to accommodate changes in settings structure.
- Improved `BillingPanel` to handle session token checks and ensure accurate billing state retrieval.
- Refactored `Rewards` page to enhance user experience with clearer progress indicators and improved layout.
- Added tests to validate changes in the rewards and settings components.

* feat(config): enhance API base URL handling and environment configuration

- Introduced a new staging API base URL constant for better environment management.
- Added app environment variable constants to streamline environment detection.
- Refactored effective API URL resolution to accommodate environment-specific defaults.
- Implemented functions to resolve app environment from process environment variables.
- Added tests to validate the correct behavior of staging and production API URL handling.

* feat(rewards): add community and referrals tabs for rewards management

- Introduced `RewardsCommunityTab` and `RewardsReferralsTab` components to enhance the rewards management interface.
- The `RewardsCommunityTab` displays user progress, Discord role statuses, and connection options, improving user engagement with rewards.
- The `RewardsReferralsTab` allows users to manage their referral program, track progress, and access coupon rewards in a streamlined layout.
- Updated the `Rewards` page to integrate these new tabs, enhancing overall user experience and navigation.

* feat(rewards): introduce ReferralRewardsSection and RewardsRedeemTab components

- Added `ReferralRewardsSection` to manage referral statistics and code application, enhancing user engagement with the referral program.
- Created `RewardsRedeemTab` to streamline the process of applying reward codes, improving the overall rewards management experience.
- Updated `Rewards` page to include the new redeem tab, allowing users to easily switch between referral and redeem functionalities.
- Refactored `RewardsCommunityTab` to adjust the referral selection handler, ensuring consistent navigation across the rewards interface.
- Removed redundant UI elements in `RewardsCouponSection` for a cleaner layout.
- Enhanced tests to cover new functionalities and ensure robust performance across the rewards system.

* feat(ui): introduce PillTabBar component and refactor SkillCategoryFilter and Rewards pages

- Added a new `PillTabBar` component to enhance tab navigation with customizable styles and item rendering.
- Refactored `SkillCategoryFilter` to utilize `PillTabBar`, improving code clarity and reducing redundancy in button rendering.
- Updated the `Rewards` page to replace the existing tab navigation with `PillTabBar`, streamlining the user interface for switching between referral, rewards, and redeem tabs.
- Enhanced the `ReferralRewardsSection` layout for better user experience and consistency across the rewards management interface.
- Improved tests to cover the new `PillTabBar` functionality and ensure robust performance across the updated components.

* fix(rewards): update placeholder and button text in RewardsCouponSection

- Changed the input placeholder from "Promo code" to "Coupon code" for clarity.
- Updated button text from "Apply code" to "Redeem Code" to better reflect the action being performed.
- Adjusted loading state text from "Applying…" to "Redeeming..." for consistency in user feedback.

* feat(composio): enhance toolkit handling and improve user messaging

- Updated the `ComposioConnectModal` to simplify the connection message, removing unnecessary wording for clarity.
- Introduced a `TOOLKIT_ALIASES` mapping in `toolkitMeta.tsx` to standardize toolkit slugs, improving consistency across the application.
- Refactored the `composioToolkitMeta` function to utilize the new slug mapping, enhancing toolkit metadata retrieval.
- Improved the `useComposioIntegrations` hook to leverage the canonicalized toolkit slugs for better integration handling.
- Adjusted the `Skills` page to normalize toolkit slugs during rendering, ensuring a consistent user experience.
- Updated tests to reflect changes in messaging and toolkit handling, ensuring robust functionality across the application.

* feat(intelligence): add new tabs for Dreams, Memory, and Subconscious features

- Introduced `IntelligenceDreamsTab`, which displays generated dreams based on daily life events, enhancing user engagement with a visually appealing layout.
- Added `IntelligenceMemoryTab` to manage actionable items, featuring search and filter capabilities for improved user interaction with memory data.
- Created `IntelligenceSubconsciousTab` to handle subconscious tasks and logs, providing users with insights and management options for their subconscious activities.
- Refactored the `RewardsCommunityTab` to remove unused Discord role status logic, streamlining the component for better performance.
- Implemented a new `PageBackButton` component for consistent navigation across settings pages.
- Enhanced the `BillingPanel` and its subcomponents to improve user experience in managing billing and subscription details, including transaction history and payment methods.
- Added new billing-related tabs for better organization and access to billing features, including `BillingOverviewTab`, `BillingPaymentsTab`, and `BillingPlansTab`.
- Updated tests to ensure functionality across new and refactored components, maintaining robust application performance.

* refactor(billing): update BillingPanel and BillingPlansTab for improved user experience

- Changed the default selected tab in `BillingPanel` from 'overview' to 'plans' to prioritize subscription management.
- Removed the `BillingOverviewTab` from the `BillingPanel`, streamlining the billing interface.
- Enhanced the `BillingPlansTab` header to clarify the purpose, changing "Explore tiers" to "Choose a Subscription Plan".
- Updated the description in `BillingPlansTab` for better clarity on payment options.
- Improved the layout of the `SubscriptionPlans` component to better highlight crypto payment options and their availability.
- Cleaned up unused code and comments for better maintainability.

* refactor(billing): streamline BillingPanel and BillingPlansTab components

- Removed unused `teamUsage` state and related API call from `BillingPanel` to simplify data management.
- Adjusted layout in `BillingPlansTab` for improved visual hierarchy and user experience.
- Cleaned up code by eliminating unnecessary comments and enhancing maintainability.

* refactor(billing): enhance SubscriptionPlans layout for improved responsiveness

- Updated the layout of the `SubscriptionPlans` component to ensure better responsiveness and visual consistency.
- Adjusted class names to include minimum height and width properties for better alignment across different screen sizes.
- Enhanced the layout of the pricing display section for improved clarity and user experience.

* refactor(billing): update subscription plans and billing components for improved clarity and user experience

- Adjusted pricing for BASIC and PRO plans to reflect new monthly and annual rates.
- Enhanced feature descriptions for subscription plans to better communicate value.
- Removed redundant UI elements in the BillingPaymentsTab and PayAsYouGoCard for a cleaner layout.
- Added loading and confirmation messages in SubscriptionPlans to improve user feedback during payment processes.
- Updated class names for better responsiveness and visual consistency across billing components.

* refactor(billing): improve error messaging and UI consistency in billing components

- Updated error message in BillingPanel to specify adding a payment card on Stripe for clarity.
- Changed header text in AutoRechargeSection to "Enable Auto-Recharge" for better user understanding.
- Modified button text in AutoRechargeSection to "Add card on Stripe" for consistency.
- Enhanced styling in PayAsYouGoCard for improved visual appeal and user interaction.
- Removed redundant UI elements in PayAsYouGoCard for a cleaner layout.

* refactor(billing): remove unused error handling and improve UI consistency across billing components

- Eliminated `setArError` prop from BillingPanel, AutoRechargeSection, and BillingPaymentsTab to streamline error handling.
- Enhanced layout and styling in BillingHistoryTab and SubscriptionPlans for better visual consistency and user experience.
- Removed the BillingOverviewTab component to simplify the billing interface and improve maintainability.

* refactor(billing): update feature descriptions and enhance SubscriptionPlans display

- Revised feature descriptions in the PLANS array for clarity and conciseness.
- Increased the number of displayed features in the SubscriptionPlans component to provide users with more information at a glance.

* refactor(billing): update billing components for improved clarity and functionality

- Revised feature descriptions in the PLANS array to reflect increased usage limits.
- Renamed header in BillingHistoryTab to "Transaction History" and updated description for clarity.
- Adjusted transaction amount formatting in BillingHistoryTab to display five decimal places.
- Removed redundant UI elements in BillingPaymentsTab to streamline the layout.
- Enhanced PayAsYouGoCard with improved credit balance display and top-up options, including validation for custom amounts.

* feat(usage): add budget completion message logic and update tests

- Introduced `shouldShowBudgetCompletedMessage` to the `UsageState` interface to indicate when the budget completion message should be displayed.
- Updated the `useUsageState` hook to calculate the budget completion message based on user credits and budget status.
- Enhanced tests for `useUsageState` to verify the correct behavior of the budget completion message under various scenarios.
- Modified the `Conversations` component to display the budget completion message appropriately based on the new logic.

* style: apply formatter fixes from pre-push hook

* refactor(rewards): update coupon section and test cases for clarity and consistency

- Renamed placeholder text and button labels in the RewardsCouponSection test to reflect updated terminology.
- Adjusted test assertions to ensure they align with the new button and placeholder names.
- Updated pricing details in billingHelpers tests to reflect new monthly and annual rates for BASIC and PRO plans.
- Enhanced the ContextGatheringStep labels for better clarity regarding email and LinkedIn processing stages.

* style(tests): format coupon code input changes for consistency

- Reformatted the coupon code input changes in the RewardsCouponSection test for improved readability and consistency.
- Ensured that the test cases maintain a uniform style for better maintainability.

* refactor: enhance accessibility and improve code structure in various components

- Added ARIA roles and attributes to the PillTabBar for better accessibility.
- Refactored the canonicalization logic for toolkit slugs into a new file for improved organization.
- Updated the IntelligenceDreamsTab and IntelligenceMemoryTab components for better readability and accessibility.
- Enhanced error handling and logging in the IntelligenceSubconsciousTab for improved debugging.
- Streamlined the ReferralRewardsSection and RewardsCommunityTab components by normalizing referral code input handling.
- Removed unused props and improved layout consistency in BillingPanel and related components.
- Updated the Skills page to handle subconscious escalation dismissals more effectively.

* feat(release): configure staging environment for deployment

- Added steps to configure the staging app environment in the release workflow.
- Set environment variables for staging, including OPENHUMAN_APP_ENV and VITE_OPENHUMAN_APP_ENV.
- Updated build and deployment steps to conditionally use staging settings based on the build target.
- Ensured proper handling of workspace paths for staging deployments.

* chore(env): add optional staging environment variable to .env.example

- Introduced OPENHUMAN_APP_ENV variable to specify the app environment as 'staging'.
- Updated .env.example to include new environment configuration for clarity.

* refactor(intelligence): streamline navigation and improve text formatting

- Simplified the navigation logic in the IntelligenceSubconsciousTab for better readability.
- Improved text formatting in the IntelligenceDreamsTab for enhanced clarity and consistency.
- Refactored import statements in toolkitMeta.tsx for better organization.
2026-04-13 18:10:07 -07:00
Steven EnamakelandGitHub f26a0c50b0 feat(referral): switch to link-based claims with flat $5 rewards (#546)
* refactor(referral): update referral system to a one-time flat reward structure

- Transitioned to a link-based referral system offering a one-time $5 credit to both the referrer and the referred user upon the first subscription payment.
- Removed the previous reward rate in basis points and eliminated recurring rewards, ensuring clarity in the referral process.
- Updated API endpoints and service methods to reflect the new `claimReferral` functionality, enhancing user experience and eligibility checks.
- Revised documentation and tests to align with the new referral structure, ensuring comprehensive coverage and understanding of the changes.

* refactor(referral): simplify JSON response handling in referral claim function

- Streamlined the `handle_referral_claim` function by removing unnecessary `as_deref()` and `filter()` calls, enhancing code clarity and performance.
- Updated the JSON response construction to eliminate redundant line breaks, improving readability without altering functionality.
2026-04-13 14:58:58 -07:00
7685e877ee feat(agent): welcome->orchestrator routing + per-agent tool scoping (#525, #526) (#544)
* feat(agent): add subagents + delegate_name fields to AgentDefinition (#525, #526)

Introduces the schema change needed to make agent definitions the single source
of truth for both direct tools and delegation targets:

- `subagents: Vec<SubagentEntry>` — declarative list of agents this agent can
  spawn, expanding at build time into synthesised delegate_* tools on the LLM's
  function-calling surface. Supports two TOML shapes via `#[serde(untagged)]`:
    * Bare string (`"researcher"`) → `SubagentEntry::AgentId`
    * Inline table (`{ skills = "*" }`) → `SubagentEntry::Skills(SkillsWildcard)`
  The `Skills` variant expands dynamically to one delegate_{toolkit} tool per
  connected Composio toolkit at runtime.

- `delegate_name: Option<String>` — optional override for the tool name this
  agent is exposed as when another agent lists it in `subagents`. Defaults to
  `delegate_{id}` when absent, lets the researcher agent be exposed as
  `research`, code_executor as `run_code`, etc.

Schema only — no runtime behavior change yet. Follow-up commits wire the field
into `collect_orchestrator_tools`, the dispatch path, and the debug dump.

TOML placement note: `subagents = [...]` must appear before the `[tools]` table
header in agent TOMLs. Once a table section opens, every subsequent top-level
key is consumed by that table, so placing `subagents` after `[tools]` parses it
as `tools.subagents` and fails deserializing ToolScope. The test doc-comment
records this constraint.

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

* feat(agent): drive orchestrator delegation tools from TOML subagents (#525, #526)

Completes the half-built migration from a hardcoded ARCHETYPE_TOOLS table
+ orphan skill_delegation.rs to a TOML-driven delegation surface where
each agent declares its subagents and the tool list is synthesised from
the registry at agent-build time.

Rewrites `collect_orchestrator_tools` to take the orchestrator's
AgentDefinition, the global AgentDefinitionRegistry, and the slice of
connected Composio integrations, then iterates the definition's
`subagents` field:

  * `SubagentEntry::AgentId` → one ArchetypeDelegationTool. The tool's
    `name()` comes from the target agent's `delegate_name` override (or
    `delegate_{id}` fallback) and its `description()` is the target's
    `when_to_use`. Editing an agent's TOML when_to_use now immediately
    updates the tool schema the orchestrator LLM sees — zero drift, no
    hardcoded description strings to keep in sync.

  * `SubagentEntry::Skills { skills = "*" }` → one SkillDelegationTool
    per connected Composio toolkit. Each routes to the generic
    skills_agent with `skill_filter` pre-populated. Empty integrations
    list (CLI path, or user not yet connected) produces zero tools
    rather than phantom `delegate_*` entries for unconnected toolkits.

Also in this commit:

- Wire the orphan `skill_delegation.rs` into `impl/agent/mod.rs` via
  `mod skill_delegation;` + `pub use SkillDelegationTool;`. The file has
  existed since earlier work but was never declared in the module tree,
  so it compiled as dead code.

- Delete the legacy `MAIN_AGENT_TOOL_ALLOWLIST` constant and the
  `main_agent_tools` filter in `tools/ops.rs`. They were documented as
  "no longer the primary source of truth" since the from_config builder
  switched to `collect_orchestrator_tools`, and grep confirms no
  external callers remain. Clean deletion.

- Delete the hardcoded `ARCHETYPE_TOOLS` const in
  `tools/impl/agent/mod.rs`. The 4-entry table has been replaced by the
  orchestrator TOML's `subagents` list (which covers those 4 plus
  archivist plus the skills wildcard), and the re-export in
  `tools/mod.rs` is removed accordingly.

- Update `agents/orchestrator/agent.toml`: add the `subagents` field
  listing researcher / planner / code_executor / critic / archivist /
  { skills = "*" }. Keep `spawn_subagent` in `[tools] named` as an
  advanced fallback so power users can still spawn custom workspace-
  override agent ids that aren't in the declarative subagents list.

- Add `delegate_name = "..."` to the 5 archetype TOMLs so the
  orchestrator LLM sees natural tool names (`research`, `plan`,
  `run_code`, `review_code`, `archive_session`) rather than the
  `delegate_<agent_id>` fallback.

- Update `agent/harness/session/builder.rs` (line ~461) to call the new
  `collect_orchestrator_tools` signature. Looks up the orchestrator
  definition from the global registry; passes an empty integrations
  slice because the builder is synchronous and cannot await Composio's
  async fetch. The channel-dispatch path will populate integrations in
  a later commit — the CLI/REPL path ships without per-toolkit delegation
  tools, which is acceptable regression since CLI users still reach
  Composio via `composio_execute` and the retained `spawn_subagent`
  fallback.

Tests:

  * 5 new unit tests in `orchestrator_tools.rs` cover the baseline
    AgentId + Skills wildcard expansion, empty-integrations edge case,
    unknown-id graceful skip, non-delegating agent with empty subagents,
    and the slug sanitiser for tool-name-safe Composio toolkit names.
  * Runs clean alongside all existing agent-module tests (323 pass;
    one pre-existing Windows-path failure in `self_healing::tests::
    tool_maker_prompt_includes_command` is unrelated to this PR and
    fails identically on the upstream baseline).

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

* feat(agent): plumb per-agent tool scoping through bus + tool loop (#525, #526)

Adds the parameter plumbing for agent-aware tool filtering without
changing any runtime behaviour. Every existing call site continues to
pass `None` / empty extras, so the LLM still sees the full unfiltered
registry — the actual routing logic that populates these fields lands
in commit 4b (dispatch.rs onboarding-flag → target_agent_id).

Why two parameters and not one filter:

  Tools in this codebase are `Box<dyn Tool>` — owned trait objects with
  no Clone impl, stored in a shared `Arc<Vec<Box<dyn Tool>>>`. We can't
  cheaply build a per-turn filtered subset of the global registry, and
  we can't mutate the Arc to remove entries. Two new parameters work
  around this without touching the global registry's lifetime model:

  * `visible_tool_names: Option<&HashSet<String>>` — whitelist filter
    applied at the iteration site inside `run_tool_call_loop`. When
    `Some(set)`, only tools whose `name()` is in the set contribute to
    the function-calling schema and are eligible for execution; every
    other tool in the combined registry is hidden from the model and
    rejected if the model emits a call for it. `None` preserves the
    legacy "everything visible" behaviour.

  * `extra_tools: &[Box<dyn Tool>]` — per-turn synthesised tools spliced
    alongside `tools_registry`. The dispatch path will use this to
    surface delegation tools (`research`, `delegate_gmail`, …) that are
    built fresh each turn from the active agent's `subagents` field and
    the current Composio integration list — tools that don't exist in
    the global startup-time registry because they depend on per-user
    runtime state. Empty slice for agents that don't delegate.

  Inside the loop, `tool_specs` is built from
  `tools_registry.iter().chain(extra_tools.iter()).filter(is_visible)`,
  and the tool-execution lookup uses the same chain + filter so the
  function-calling schema and the execution surface stay in sync.

Files touched in this commit:

  src/openhuman/agent/harness/tool_loop.rs
    - Add `visible_tool_names` and `extra_tools` parameters to
      `run_tool_call_loop`. Build `tool_specs` from chained iteration
      with the visibility filter applied. Replace the `find_tool` call
      at the execution site with an inline chain+filter lookup so
      hallucinated calls to filtered-out tools surface as "unknown
      tool" errors. Drop the now-unused `find_tool` import.
    - Update the legacy `agent_turn` wrapper to pass `None, &[]`,
      preserving its existing unfiltered behaviour.
    - Update all 9 in-file test sites to pass `None, &[]`.

  src/openhuman/agent/harness/tests.rs
    - Update all 3 `run_tool_call_loop` test sites to pass `None, &[]`.

  src/openhuman/agent/bus.rs
    - Add `target_agent_id: Option<String>`, `visible_tool_names:
      Option<HashSet<String>>`, and `extra_tools: Vec<Box<dyn Tool>>`
      fields to `AgentTurnRequest`, with rustdoc explaining each.
    - Destructure the new fields in the `agent.run_turn` handler;
      thread `visible_tool_names.as_ref()` and `&extra_tools` through
      to `run_tool_call_loop`. Augment the dispatch trace with
      target_agent / extra_tool_count / visible_tool_count /
      filter_active so production logs show whether scoping is active.
    - Update the in-test `test_request()` helper to populate the new
      fields with safe defaults.

  src/openhuman/agent/triage/evaluator.rs
    - Update the triage `AgentTurnRequest` initializer to set
      `target_agent_id = Some("trigger_triage")` (for tracing) with
      `visible_tool_names: None` + `extra_tools: Vec::new()` because
      the classifier intentionally runs against an empty registry and
      emits a structured JSON decision rather than calling tools.

  src/openhuman/channels/runtime/dispatch.rs
    - Update the channel-message `AgentTurnRequest` initializer to set
      the three new fields to safe defaults (`None` / `None` / empty
      vec). Commit 4b will replace these with the real onboarding-flag
      based routing.

  src/openhuman/tools/impl/agent/mod.rs
    - Bug fix: `dispatch_subagent` previously took `_skill_filter:
      Option<&str>` but discarded the value, hardcoding
      `SubagentRunOptions::skill_filter_override = None`. That meant
      `SkillDelegationTool::execute()` synthesising
      `dispatch_subagent("skills_agent", ..., Some("gmail"))` never
      actually narrowed `skills_agent`'s tool list — so even with the
      orchestrator's view scoped, the spawned `skills_agent` subagent
      would still see the full Composio catalog. Drop the underscore,
      propagate `skill_filter` into `skill_filter_override`, and add a
      tracing log line to make this path observable. This is the
      downstream half of the #526 leak that commit 3's orchestrator-
      side scoping alone wouldn't have caught.

Tests: 8/8 `tool_loop` tests pass, 3/3 harness `tests.rs` cases pass,
323/324 agent module tests pass overall (the one failure is the same
pre-existing Windows-path bug in `self_healing::tool_maker_prompt_
includes_command` that fails identically on the upstream baseline).
No existing test expectations were changed.

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

* feat(dispatch): route channel messages to welcome/orchestrator by onboarding flag (#525)

Wires the per-agent tool scoping plumbing from commit 4a into the
channel-message dispatch path. Each incoming channel message now picks
the active agent — `welcome` pre-onboarding, `orchestrator` post — based
on `Config::onboarding_completed`, loads the matching definition from
the global `AgentDefinitionRegistry`, synthesises any `delegate_*` tools
the agent declares in its `subagents` field, and passes everything
through to `agent.run_turn` on the bus.

This is the half of #525 that makes the welcome agent actually run for
new users — the welcome definition has existed since upstream PR #522
but had no caller; nothing in dispatch consulted the onboarding flag,
so every channel message ran through the same generic tool loop with
the full registry exposed.

Changes:

  src/openhuman/channels/runtime/dispatch.rs
    - New `AgentScoping` struct carrying the three new `AgentTurnRequest`
      fields (`target_agent_id`, `visible_tool_names`, `extra_tools`)
      plus an `unscoped()` constructor for safe-fallback paths.
    - New async `resolve_target_agent(channel)` helper:
      * fresh `Config::load_or_init().await` per turn (no cache — the
        loader reads from disk every call, verified at
        `config/schema/load.rs:409`, so the welcome→orchestrator
        handoff is observed on the next message after
        `complete_onboarding(complete)` flips the flag, with no need
        for an explicit handoff event);
      * picks `"welcome"` or `"orchestrator"` based on the flag and
        emits a structured `[dispatch::routing] selected target agent`
        info trace recording the choice + the flag value, satisfying
        the #525 acceptance criterion `"agent-selection logs clearly
        record why each agent was selected at onboarding boundaries"`;
      * looks up the definition in `AgentDefinitionRegistry::global()`,
        gracefully falling back to `AgentScoping::unscoped()` (= legacy
        behaviour, no filter, no extras) if the registry isn't
        initialised or the definition isn't found, so a routing miss
        never fails the user message;
      * for agents with a non-empty `subagents` field, awaits
        `composio::fetch_connected_integrations(&config)` and runs
        `orchestrator_tools::collect_orchestrator_tools` to materialise
        per-turn delegation tools (`research`, `plan`, `delegate_gmail`,
        …). Agents with empty `subagents` get an empty extras vec.
    - New `build_visible_tool_set(definition, &extra_tools)` helper that
      returns `Some(union)` for `ToolScope::Named` agents (their named
      list ∪ the names of the synthesised delegation tools) and `None`
      for `ToolScope::Wildcard` agents to preserve the unfiltered
      semantics — so agents like `skills_agent` and `morning_briefing`
      that already work via `wildcard + category_filter` keep their
      existing behaviour without this layer interfering.
    - `process_channel_message` calls `resolve_target_agent` once per
      turn, drops the placeholder defaults from commit 4a, and feeds
      the real `target_agent_id`/`visible_tool_names`/`extra_tools`
      into `AgentTurnRequest`.
    - New imports: `AgentDefinition`, `AgentDefinitionRegistry`,
      `ToolScope`, `Config`, `fetch_connected_integrations`,
      `orchestrator_tools`, `Tool`, `HashSet`.

End-to-end behaviour after this commit:

  1. New user, `onboarding_completed=false`: dispatch picks `welcome`,
     loads its 2-tool TOML scope, builds `visible_tool_names =
     {complete_onboarding, memory_recall}`, no extras, hands off to
     the bus. Bus handler applies the filter → welcome's LLM sees
     exactly 2 tools.
  2. Welcome agent guides the user through setup, eventually calls
     `complete_onboarding(action="complete")` → flag persists to disk
     via `config.save()`.
  3. Next user message: dispatch reads the flag fresh, picks
     `orchestrator`, fetches connected Composio integrations, expands
     `subagents = ["researcher", "planner", "code_executor", "critic",
     "archivist", { skills = "*" }]` into delegate_research /
     delegate_plan / delegate_run_code / delegate_review_code /
     delegate_archive_session + one delegate_<toolkit> per connected
     integration. visible_tool_names is the union with the 4 direct
     tools from orchestrator's `[tools] named` list. LLM sees the
     scoped delegation surface, not the full 1000+ Composio catalog.

#526's runtime leak is now fixed end-to-end: the orchestrator's LLM
prompt only contains the tools its TOML allows, and the
SkillDelegationTool path narrows skills_agent to a single toolkit via
the `skill_filter` propagation fix from commit 4a. No agent at any
layer sees more than its definition declares.

Tests: 599/599 channel module tests pass — including
`runtime_dispatch::dispatch_routes_through_agent_run_turn_bus_handler`
and the telegram integration variant, which exercise the full bus
roundtrip with the new fields populated. No existing assertions were
modified.

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

* feat(debug_dump): apply orchestrator definition filter to main dump (#526)

Replaces the explicit `empty_filter: HashSet::new()` in
`render_main_agent_dump` with a real visibility whitelist derived from
the orchestrator's `AgentDefinition`. The "main" dump path now mirrors
the runtime dispatch path from commits 4a/4b — same definition, same
delegation tool synthesis, same filter — so `openhuman agent dump-prompt
main` shows what the LLM actually sees in production instead of the
unfiltered global registry.

Before this commit, `dump-prompt main` always rendered every tool in
the registry regardless of which agent was supposed to run, which is
exactly the symptom #526 reports: "agent prompt tool scopes leak full
GitHub tool catalog". The runtime fix in commit 4b stops the leak in
production but the dump path was the user's primary observability tool
for inspecting prompts, so leaving it unscoped would mask future
regressions and confuse debug sessions.

Changes in `src/openhuman/context/debug_dump.rs`:

  * `dump_agent_prompt` now loads `AgentDefinitionRegistry` once at the
    top (previously only loaded inside the sub-agent branch). When the
    request is for "main" or "orchestrator_main", it looks up the
    "orchestrator" definition from the registry and passes it into
    `render_main_agent_dump` along with the registry itself. Missing
    orchestrator entry → structured error listing known agents
    instead of silently rendering an unfiltered prompt.

  * `render_main_agent_dump` signature gains `registry:
    &AgentDefinitionRegistry` and `orchestrator_def: &AgentDefinition`.
    Inside, it:
      - calls `collect_orchestrator_tools(orchestrator_def, registry,
        connected_integrations)` to synthesise the same per-turn
        delegation tools (`research`, `plan`, `delegate_<toolkit>`,
        …) that dispatch generates;
      - extends `prompt_tools` with the synthesised extras so they
        contribute to the rendered tool catalogue;
      - builds `visible_filter: HashSet<String>` from
        `orchestrator_def.tools` (the `[tools] named` list) ∪ the
        names of the synthesised extras, falling back to an empty
        HashSet when the orchestrator definition uses
        `ToolScope::Wildcard` (which the prompt builder treats as "no
        filter, every tool visible") so dump consumers that supply a
        wildcard orchestrator (custom workspace overrides, tests)
        retain the legacy unscoped behaviour;
      - replaces `visible_tool_names: &empty_filter` in the
        `PromptContext` with `&visible_filter`;
      - filters the returned `tool_names` and `skill_tool_count` by
        the same predicate so the `DumpedPrompt` summary fields
        match what the prompt text actually contains.

Tests:

  * Replaces the previous `render_main_agent_dump_includes_tool_
    instructions_and_skill_count` test with two more focused cases:

    1. `render_main_agent_dump_wildcard_scope_shows_full_tool_set` —
       regression guard for the legacy wildcard path. Builds a
       wildcard-scoped orchestrator definition, asserts every tool
       from `tools_vec` survives, and checks the standard system-
       prompt skeleton (Tools section, Tool Use Protocol, cache
       boundary) still renders.

    2. `render_main_agent_dump_named_scope_filters_to_whitelist` —
       the #526 regression guard. Builds an orchestrator with
       `ToolScope::Named(["query_memory", "ask_user_clarification"])`
       and a `tools_vec` containing `shell`, `query_memory`, and
       `GMAIL_SEND_EMAIL`. Asserts the dump's `tool_names` is exactly
       `["query_memory"]` — `shell` and `GMAIL_SEND_EMAIL` are in the
       global registry but NOT in the whitelist, so they MUST be
       excluded. If a future change reintroduces the unfiltered
       behaviour this test fails immediately.

  * Adds two test helpers: `wildcard_orchestrator_def()` builds a
    minimal orchestrator definition with all `omit_*` flags set and
    `ToolScope::Wildcard`, and `registry_with_orchestrator(orch)`
    wraps it in an `AgentDefinitionRegistry` so the tests can call
    `render_main_agent_dump` without going through the full TOML
    loader.

11/11 debug_dump tests pass. The two new guards plus the 9 existing
sub-agent / filter / composio-stub tests all run clean.

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

* test(dispatch): unit tests for build_visible_tool_set + cargo fmt cleanup (#525, #526)

Adds focused unit tests for the per-agent scoping helper landed in
commit 4b and runs `cargo fmt` across the files touched by this PR.

Why scoping unit tests, not full integration tests:

  `resolve_target_agent` is async and reads `Config::load_or_init().await`
  which does a real disk read every call (no cache, verified at
  `config/schema/load.rs:409`). Mocking that requires either spinning up
  a full workspace under a temp dir with a config.toml containing the
  right `onboarding_completed` value, or adding a test-only injection
  point on the public Config API. Both are tractable but invasive
  enough to belong in their own follow-up PR. The end-to-end dispatch
  path is already covered by the existing channel integration tests
  (`dispatch_routes_through_agent_run_turn_bus_handler` etc.) which
  exercise the full bus roundtrip with the new fields populated, and
  which still pass after the new resolver landed (it gracefully falls
  back to `AgentScoping::unscoped()` when no orchestrator definition
  is registered in the test environment).

  Pure-function unit tests for `build_visible_tool_set` cover the
  branching logic that does the actual scoping work: how the named
  whitelist + extras union is built, how Wildcard scope is preserved,
  how duplicates are de-duplicated, etc. That's the part most likely
  to drift in future changes, so it's the part most worth fencing
  with focused tests.

Tests added (all in `src/openhuman/channels/runtime/dispatch.rs` under
the new `scoping_tests` module):

  * `wildcard_scope_yields_none_filter` — `ToolScope::Wildcard` must
    produce `None` regardless of whether extras are present, so
    skills_agent / morning_briefing keep their full skill-category
    catalogue.

  * `named_scope_without_extras_returns_named_only` — the welcome
    agent's path: 2 named tools, no delegation, exactly 2 entries in
    the visibility whitelist.

  * `named_scope_with_extras_returns_union` — the orchestrator's path:
    3 direct named tools + 3 synthesised extras (research,
    delegate_gmail, delegate_github) → 6 entries.

  * `empty_named_with_extras_returns_extras_only` — guards a future
    "delegation-only" agent layout where the agent has no direct tools
    of its own, just spawns subagents.

  * `empty_named_with_no_extras_returns_empty_set` — guards the
    distinction between `None` (no filter, all visible) and
    `Some(empty)` (filter active, nothing matches). Important because
    the prompt loop's `is_visible` check treats them differently.

  * `duplicate_names_across_named_and_extras_are_deduplicated` — the
    HashSet handles collisions automatically, but the test pins that
    behaviour so a future migration to `Vec<String>` (which would
    silently double-count) gets caught.

  * `agent_scoping_unscoped_has_no_filter_or_extras` — pins the
    safe-fallback constructor's contract. Used when the registry is
    uninitialised or the target agent is missing — every field must
    default to "no scoping" so the channel turn falls back to legacy
    unfiltered behaviour rather than crashing.

Plus `cargo fmt` run across the 6 files modified by this PR. No
behavioural changes.

Final test status across all commits 2-6 in this PR:

  * agent::harness::definition: 10/10  (4 new for Subagents schema)
  * agent::harness::tool_loop: 8/8 
  * agent::harness::tests: 3/3 
  * tools::orchestrator_tools: 5/5  (5 new)
  * channels::*: 599/599  (incl. dispatch integration)
  * channels::runtime::dispatch::scoping_tests: 7/7  (7 new)
  * context::debug_dump: 11/11  (1 replaced + 1 new)
  * Total agent module: 323/324 (one pre-existing Windows path
    failure in `self_healing::tool_maker_prompt_includes_command`
    confirmed identical against upstream/main baseline)

Pre-existing Windows-environment test failures NOT caused by this PR
and out of scope (all confirmed identical on upstream baseline; CI on
Linux is unaffected):

  * self_healing::tool_maker_prompt_includes_command (PathBuf separator)
  * cron::scheduler::run_job_command_success / _failure (Unix shell)
  * composio::trigger_history::archives_triggers_in_daily_jsonl... (path)
  * local_ai::paths::target_paths_preserve_absolute_overrides (path)
  * security::policy::checklist_root_path_blocked (POSIX absolute)
  * security::policy::checklist_workspace_only_blocks_all_absolute (POSIX)
  * tools::implementations::browser::screenshot::screenshot_command_
    contains_output_path (browser binary lookup)

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

* feat(welcome): upgrade bare-install nudge with concrete integration pitches (#525)

Follow-up to #522 that addresses a rough UX edge in the welcome agent
prompt: users who arrive with only an API key configured (no
channels, no Composio integrations, no web search / browser / local
AI) were getting a "gentle suggestion" to connect something without
any concrete picture of what they'd actually unlock. The welcome
agent would finish onboarding, hand off to orchestrator, and leave
the user staring at a functional-but-empty assistant with no
roadmap for how to make it useful.

This commit rewrites the prompt to handle sparse setups explicitly.
No Rust changes — this is prompt.md only, picked up at build time
via the existing `include_str!` loader in `agents/mod.rs`.

Changes to `src/openhuman/agent/agents/welcome/prompt.md`:

  * Step 2's "point out what's missing" sub-point is rewritten from
    a single "gently suggest" line into a four-case decision tree
    keyed off `check_status` state:
      - No API key → critical, block completion.
      - Integrations yes, channels no → note the Tauri-only reach
        limitation, suggest a messaging platform.
      - Channels yes, integrations no → degraded assistant, nudge
        toward Composio.
      - Nothing beyond the API key → the "bare install" case, gets
        the new Step 2.5 treatment.

  * New **Step 2.5: Handling a bare install** section added after
    Step 2. Spells out what the user DOES have (sandboxed reasoning
    + coding assistant with memory), what they're MISSING (any
    external action), and how to structure the message: state the
    current capability honestly, pitch 2-3 specific integrations
    with concrete example prompts, point to Settings → Integrations
    / Channels, and leave room for the user to opt into the
    coding-only experience if that's what they actually want. For
    bare-install users the word budget stretches to 250-400 words
    (up from 200-350) so the concrete pitches and example prompts
    actually fit without cramming.

  * New **Integration capability reference** section giving the LLM
    a menu it can draw from when pitching integrations. Each entry
    is a one-line "connect X → I can Y" with a concrete example
    prompt the user could send next:
      - Gmail: "Summarise the most important emails that came in
        overnight and flag anything that needs a reply today."
      - Google Calendar: "What's on my calendar tomorrow, and do I
        have a 30-minute gap before 2pm?"
      - GitHub: "List open issues on my main project tagged 'bug'
        and summarise which ones look newest or most urgent."
      - Notion: "Pull up my 'Ideas' Notion database and show me
        the three newest entries."
      - Slack / Discord / Linear / Jira / etc. with similar shapes.

    Plus a sub-section for messaging platforms (Telegram / Discord /
    Slack / iMessage / WhatsApp / Signal / web-fallback) that
    clarifies which each is best for, and a sub-section for the
    other capabilities (web search, browser automation, HTTP
    requests, local AI) that explains what breaks without them.

    The LLM is told NOT to list everything — just pick 2-3 most
    likely to matter, defaulting to Gmail + GitHub + one of
    {Calendar, Notion} as the top-3 pitch when no profile context
    is available.

  * Tone guidelines updated to document the stretched word budget
    for bare installs (200-350 for configured users, 250-400 for
    bare installs).

  * "What NOT to do" list updated:
      - Explicitly allows product-tour-style listing ONLY in the
        bare-install case (Step 2.5), forbids it elsewhere.
      - Clarifies that describing what WOULD unlock with integration
        X is fine and encouraged; claiming a capability the user
        doesn't have is still forbidden.
      - Adds a new "Don't gloss over a bare install" entry that
        pins the rule: API-key-only users get concrete pitches and
        example prompts, not vague suggestions.

Scope note: this commit does NOT change the completion logic.
`complete_onboarding(complete)` still accepts API-key-only as the
minimum bar — that's a separate design question for the maintainer
about whether zero-integration users should be gatekept. This
change improves what the welcome agent SAYS to those users, not
whether they're allowed to proceed.

Tests: all 14 `agent::agents::tests` pass (including
`welcome_has_onboarding_and_memory_tools` which validates the
welcome agent's declarative shape is unchanged). The prompt.md
edit is pure content — no schema changes, no tool additions.

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

* feat(web-channel): route Tauri in-app chat to welcome/orchestrator + Windows fsync fix (#525)

Closes the web-channel-side half of the #525 welcome agent routing.
Also bundles a small pre-existing Windows compatibility fix for
`app_state::ops::sync_parent_dir` that was blocking end-to-end testing
of this branch on Windows.

## Web channel routing (primary change)

Commit 4b (274b50ed) wired the welcome-vs-orchestrator routing into
`channels/runtime/dispatch.rs::process_channel_message` — the handler
for inbound messages on external channels (Telegram, Discord, Slack,
iMessage, etc.). That path reads `config.onboarding_completed` and
selects the right agent via the bus.

End-to-end testing on the Tauri desktop app revealed a gap: the
in-app chat window does NOT route through `process_channel_message`.
It uses its own JSON-RPC method (`openhuman.channel_web_chat` →
`start_chat` → `run_chat_task` → `build_session_agent` →
`Agent::from_config`) which was hardcoded to build a generic
main-agent every turn. So a new user typing in the Tauri app saw the
orchestrator immediately, never the welcome agent.

This commit closes that gap without adding a third code path:

### `src/openhuman/agent/harness/session/builder.rs`

* Split the existing `Agent::from_config` into a thin wrapper + a
  new `Agent::from_config_for_agent(config, agent_id)`. The wrapper
  calls the new function with `agent_id = "orchestrator"`, preserving
  the legacy behaviour byte-identically for all existing callers
  (CLI, REPL, tests, every call site that doesn't care which agent
  they get).
* `from_config_for_agent` looks up `agent_id` in
  `AgentDefinitionRegistry::global()` up front; unknown ids fail
  fast with a clear error. Missing orchestrator is tolerated as a
  legacy / pre-startup fallback so existing tests that run without
  a populated registry continue to work.
* When a target definition is resolved:
    - `prompt_builder` uses `SystemPromptBuilder::for_subagent` with
      the agent's `system_prompt` body (read from the registry,
      which already has it inlined via `include_str!` from
      `agents/*/prompt.md` at crate-build time) and the three
      `omit_*` flags from its TOML.
    - `visible_tool_names` is built from the agent's
      `ToolScope::Named` list, unioned with the names of any
      synthesised delegation tools produced by
      `collect_orchestrator_tools` (for agents that declare
      `subagents = [...]`).
    - `ToolScope::Wildcard` yields an empty filter, matching the
      legacy "no filter, all visible" semantics.
    - `temperature` comes from the agent's TOML (e.g. welcome is
      0.7, orchestrator is 0.4) instead of
      `config.default_temperature`.
* Legacy behaviour for plain `from_config(config)` — which passes
  `agent_id = "orchestrator"` — is preserved: the prompt builder
  continues to use `SystemPromptBuilder::with_defaults()`, temperature
  comes from `config.default_temperature`, and the orchestrator's
  `subagents` list drives delegation tool synthesis exactly as
  before. No test expectations changed.

### `src/openhuman/channels/providers/web.rs`

* `build_session_agent` now reads `config.onboarding_completed` and
  picks `"welcome"` or `"orchestrator"` as the target agent id, then
  calls `Agent::from_config_for_agent`. Structured info log records
  the routing decision + the flag state for observability, matching
  the `[dispatch::routing]` trace pattern from Commit 4b.
* `config.onboarding_completed` is read fresh every turn because
  `run_chat_task` loads the config via
  `config_rpc::load_config_with_timeout`, which reads from disk on
  every call (no in-process cache). Welcome → orchestrator handoff
  therefore happens automatically on the next chat message after
  `complete_onboarding(complete)` flips the flag, with no explicit
  event or notification plumbing.
* `set_event_context` call is unchanged — the event context is a
  (session_id, channel_name) pair for telemetry, not the agent id.

## Windows fsync fix (bundled)

### `src/openhuman/app_state/ops.rs`

`sync_parent_dir` was calling `File::open(parent).and_then(|dir|
dir.sync_all())` on the save path for `app_state.json`. On Windows,
opening a directory as a regular file requires
`FILE_FLAG_BACKUP_SEMANTICS` which `std::fs::File::open` does not
set, so the call fails with "Access is denied. (os error 5)".

Mirrored the existing `#[cfg(unix)]` guard already in
`config/schema/load.rs::sync_directory`. On non-Unix the function is
a no-op and returns `Ok(())`. Durability on Windows is provided by
`NamedTempFile::persist`'s atomic `MoveFileEx`, which is sufficient
for config files.

This is a pre-existing upstream bug, not caused by this PR, but it
blocks end-to-end testing of any code path that touches
`app_state::save_stored_app_state_unlocked` on Windows — which
includes the web channel's agent_server_status RPC. Fixing it here
so the #525 routing can actually be tested on the developer's
machine, and the fix is tiny and self-contained. The `File` import
on line 1 is also gated behind `#[cfg(unix)]` to avoid an
unused-import warning on Windows.

## Tests

All 35 `agent::harness::session` tests pass, including the
transcript round-trip + session queue + runtime tests that exercise
the builder path most heavily. No test expectations were modified;
the refactor is a pure delegation chain (`from_config` → new inner
method → existing builder).

CLI dump-prompt testing from the earlier session still validates:
  * `openhuman agent dump-prompt welcome` → 2 tools, Step 2.5
    bare-install prompt embedded
  * `openhuman agent dump-prompt main` → 6 tools, zero skill leakage
  * `openhuman agent dump-prompt main --stub-composio` → still 6
    tools, composio meta-tools correctly filtered out

End-to-end Tauri testing (next step after this commit) will exercise
the new `build_session_agent` branch live — user types `hi`, web
channel routes to welcome, welcome replies with the updated
bare-install prompt from commit 990a7264.

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

* feat(config): split chat_onboarding_completed from React UI onboarding flag (#525)

End-to-end testing on the Tauri desktop app revealed that the welcome
agent could never run from the in-app chat pane — even with all of
Commits 4b/8 in place — because the React layer's
`OnboardingOverlay.tsx` renders a full-screen wizard whenever
`onboarding_completed = false` and gates the chat pane behind it. By
the time a user can type a chat message the React wizard has already
flipped `onboarding_completed = true` (via `OnboardingOverlay::handleDone`
or the `Onboarding.tsx` wizard's completion handler), so the welcome
agent's routing condition is never satisfied on the Tauri surface.

This commit fixes the architectural conflict by splitting the single
`onboarding_completed` flag into two orthogonal flags:

* **`onboarding_completed`** — unchanged semantics. Tracks whether the
  React UI wizard has been completed/dismissed. Continues to be set
  by `OnboardingOverlay.tsx::handleDone` and `Onboarding.tsx` via the
  existing `config.set_onboarding_completed` JSON-RPC method. Used
  exclusively to gate whether the React layer renders the wizard.

* **`chat_onboarding_completed`** — NEW. Tracks whether the welcome
  agent's chat-based greeting flow has run. Set exclusively by the
  welcome agent itself via `complete_onboarding(action="complete")`.
  Used by both the Tauri web channel
  (`channels::providers::web::build_session_agent`) and the external
  channel dispatch path (`channels::runtime::dispatch::resolve_target_agent`)
  to decide whether to route to welcome or orchestrator.

The two flags are intentionally orthogonal:

  * A Tauri desktop user completes the React wizard → only
    `onboarding_completed` flips → wizard disappears → user types `hi`
    → welcome agent runs (because `chat_onboarding_completed` is still
    false) → welcome calls `complete_onboarding(complete)` →
    `chat_onboarding_completed` flips → next chat turn routes to
    orchestrator.

  * A Telegram/Discord user (no React wizard exists) sends a message
    → external channel dispatch checks `chat_onboarding_completed` →
    routes to welcome → welcome runs → flips the flag → next inbound
    message routes to orchestrator.

Both paths give every user the chat welcome experience, regardless of
which surface they came in through, and without requiring the React
wizard to be removed or restructured.

## Files changed

`src/openhuman/config/schema/types.rs`
* Add `chat_onboarding_completed: bool` field with `#[serde(default)]`
  for backward compat — existing `config.toml` files that don't have
  the field default to `false`, which means existing users will see
  the welcome agent on their next chat turn (correct behaviour, the
  welcome agent is idempotent).
* Default impl initializes the new field to `false`.
* Extensive rustdoc on both flags explaining the orthogonal split,
  why two flags exist, and which code paths gate on which.

`src/openhuman/tools/impl/agent/complete_onboarding.rs`
* `check_status` now reports BOTH flags side-by-side so the welcome
  agent's LLM can see whether the React wizard has run AND whether
  the chat welcome itself has run. Old single "Onboarding completed"
  line replaced with two lines: "UI onboarding wizard completed" and
  "Chat welcome flow completed".
* `complete()` now flips `chat_onboarding_completed`, NOT
  `onboarding_completed`. The React UI's flag is left untouched —
  that's owned by the React layer. Idempotency guard updated to
  check the chat flag.
* Rustdoc on `complete()` explains the orthogonal-flags rationale
  for future readers.

`src/openhuman/channels/providers/web.rs::build_session_agent`
* Reads `effective.chat_onboarding_completed` instead of
  `effective.onboarding_completed` for the welcome-vs-orchestrator
  decision.
* Log line now includes BOTH flags so observability captures the
  full picture (e.g. `chat_onboarding_completed=false,
  ui_onboarding_completed=true` is the expected steady state for a
  Tauri user who completed the React wizard but hasn't typed yet).

`src/openhuman/channels/runtime/dispatch.rs::resolve_target_agent`
* Same flag swap for the external-channel path.
* `[dispatch::routing] selected target agent` info trace also
  reports both flags.
* Function-level rustdoc updated with the new semantics.

## Backward compatibility

* Existing `config.toml` files: `chat_onboarding_completed` defaults
  to `false` via `#[serde(default)]`. Means existing users get a
  welcome message on their next chat turn — this is the desired
  behaviour, not a regression.
* React layer: untouched. The `config.set_onboarding_completed`
  JSON-RPC method continues to write the same field; the new flag is
  not exposed to React at all.
* External callers of `complete_onboarding(complete)`: they now flip
  a different flag. This may matter for callers who were depending
  on the flag flip side effect; a quick grep shows
  `tools/impl/agent/complete_onboarding.rs` is the only caller and
  the rest of the codebase reads `onboarding_completed` for UI
  purposes (snapshots, app state) and `chat_onboarding_completed`
  for routing — the split is clean.

## Tests

399/400 tools tests pass. The single failure
(`tools::impl::browser::screenshot::screenshot_command_contains_output_path`)
is a pre-existing Windows-environment bug that fails identically on
`upstream/main` baseline and is unrelated to this PR. No test
expectations were modified.

## Next step

Restage sidecar, relaunch Tauri, click through the React wizard once
to dismiss it, then type `hi` in the chat pane. This time
`build_session_agent` should read `chat_onboarding_completed=false`
and route to welcome, even though `onboarding_completed=true` was set
by the React layer.

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

* fix(welcome): force tool-call-first iteration to stop greeting fallback (#525)

End-to-end testing on the Tauri desktop app revealed that the welcome
agent — even with all routing/scoping commits in place and the
correct prompt embedded — was producing 1-line greetings like
"Hey! What's up?" (15 chars, single iteration, zero tool calls)
instead of running its workflow. The user typed `hihi`, the LLM saw
the welcome agent's full ~14KB persona prompt, and chose to ignore
every workflow step in favour of the chatbot fallback behaviour.

Diagnosis: the previous prompt was descriptive ("Call check_status to
get a snapshot...") rather than imperative. Combined with a "Concise"
tone guideline and a low-information user input ("hihi"), the model
under tension between "be warm and concise" and "follow the workflow"
collapsed to "tiny greeting" — which is the chatbot training-data
default for short user messages.

This commit makes the workflow non-negotiable by:

## 1. New "MANDATORY FIRST ACTION" preamble at the top of the prompt

Inserted as a blockquote between the role description and "Your
workflow" so the model encounters it before any workflow steps. The
preamble:

* States explicitly that the **first thing emitted on every turn must
  be a tool call** to `complete_onboarding(check_status)` — before
  any user-facing text, before any greeting, before any thinking out
  loud.
* States the user's input is **irrelevant** to this rule: "hi",
  "hello", emoji, nothing — the first iteration always emits the
  same thing, a check_status tool call.
* Explains *why* the rule exists (without a status snapshot the
  agent has nothing to personalise on, and any blind greeting
  defeats the welcome agent's one job).
* Shows three concrete  wrong examples (generic chitchat reply,
  greeting-then-tool, refusing the tool because the user said hi)
  and one  correct example (first iteration emits ONLY the tool
  call, message comes in iteration 2).
* Closes with a stop-and-correct rule: "If you find yourself about
  to write any text in your first iteration, STOP. Emit the tool
  call instead."

## 2. Strengthened "Your workflow / Step 1" heading

Renamed to "Step 1: Check setup status (ALWAYS — see Mandatory First
Action above)" so the cross-reference is unmissable. Body explicitly
says "In your **first iteration**, ... No text. No greeting. Just
the tool call. ... You will use this report to write the actual
welcome message in your second iteration."

## 3. Length is non-negotiable (rewritten "Concise" tone guideline)

The previous "Concise — but scale with the situation" framing was the
exact phrase the model latched onto when producing 15 chars. Rewritten
to "Length is non-negotiable" and adds:

* "A 1-2 sentence greeting is a failure, not a 'concise' success."
* Explicit "if you ever produce a message under 100 words, stop and
  try again — you've almost certainly skipped a required element."
* A "concise vs. terse" callout distinguishing "no wasted words in a
  message that does its full job" from "skip the job entirely."

## 4. New "What NOT to do" entries

Three new bullets target the exact failure modes observed:

* The existing "Don't skip check_status" entry is upgraded with
  "**This is the single most common failure mode**" and a pointer
  back to the mandatory-first-action preamble.
* New "Don't reply with a 1-line greeting" entry forbids the chatbot
  fallback explicitly and sets a 100-word floor.
* New "Don't treat 'hi' / 'hello' / short greetings as a signal to
  be brief" entry explicitly disconnects user input length from
  agent output length, since "hi" is the most common opening and
  the user typing it needs the FULL welcome experience.

## What this does NOT change

* No Rust code changes. `prompt.md` is `include_str!`'d into the
  binary at crate-build time via `agents/mod.rs`, so the next sidecar
  rebuild picks up the new content automatically.
* No agent definition changes (`agent.toml` untouched). The welcome
  agent still has 2 tools (`complete_onboarding` + `memory_recall`),
  `temperature = 0.7`, `max_iterations = 6`, `omit_*` flags
  unchanged.
* No model hint change. Still `"agentic"`. If the new prompt language
  alone isn't enough to push the model over the workflow-following
  threshold, a follow-up commit can bump to `"reasoning"` for
  stronger instruction-following.
* The integration capability reference, Step 2.5 bare-install
  handling, subscription/referral flow, and handoff sections are
  unchanged from commit 990a7264. Those were never the problem —
  the problem was the model never reaching them because it skipped
  Step 1 entirely.

## Tests

14/14 `agent::agents::tests` pass, including
`welcome_has_onboarding_and_memory_tools` and `every_builtin_has_a_
prompt_body`. The structural shape of the welcome agent definition
is byte-identical; only the embedded prompt body changed.

## Verification plan

After restaging the sidecar and relaunching Tauri, type `hi` in the
chat pane. Expected behaviour:

1. `[web-channel] routing chat turn to 'welcome'` (already validated
   in commit 56d95e2c).
2. `[agent::builder] building session agent id=welcome` (already
   validated).
3. **`[agent_loop] iteration start i=1`** with a `check_status` tool
   call as the first emission — this is the new behaviour we're
   verifying.
4. `[agent_loop] iteration start i=2` after the tool returns, with
   the actual welcome message — should be 100-400 words covering all
   required elements.
5. If everything's in place, `complete_onboarding(complete)` call in
   the same turn or a later iteration → `chat_onboarding_completed`
   flips to `true` → next chat turn routes to orchestrator.

If iteration 1 still produces text instead of a tool call, the next
escalation is bumping the model hint from `"agentic"` to
`"reasoning"` in `agent.toml`.

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

* fix(complete_onboarding): check session JWT in addition to legacy api_key (#525)

Discovered during end-to-end Tauri testing that the welcome agent
refused to call `complete_onboarding(complete)` for a fully
authenticated user because `check_status` reported "API key:
**missing**" — even though the user had completed the desktop OAuth
flow and a valid encrypted JWT was sitting in `auth-profiles.json`.

## Root cause

`check_status` was reading only `config.api_key` (an `Option<String>`
on the Config struct), which is a **legacy free-form provider key
field**. It is not where the desktop OAuth flow stores its
credentials. The actual openhuman backend session JWT lives in
`<openhuman_dir>/auth-profiles.json` under the `app-session:default`
profile, encrypted via the SecretStore using `<openhuman_dir>/.secret_key`,
and is read at every inference RPC via
`crate::api::jwt::get_session_token(config)` (which delegates to
`AuthService::from_config(config).get_profile(APP_SESSION_PROVIDER, None)`).

So a user who:
1. Completed the desktop deep-link OAuth flow (the only supported
   way to get an account), and
2. Has a fully populated `auth-profiles.json` with a valid encrypted
   `app-session:default` token,

was reported by `check_status` as "API key missing" because their
JWT lives in the auth profile store, not in `config.api_key`. The
welcome agent then dutifully refused to call `complete_onboarding(
complete)` per its own prompt rule ("If critical setup is missing,
do not complete onboarding"), and re-ran on every chat turn forever
even though there was nothing to fix.

This is a **pre-existing upstream bug** dating from PR #522 (the
welcome agent's introduction). It was written before, or in parallel
with, the auth-profile refactor that moved session credentials out of
`config.api_key` into the dedicated profile store. The check was
never updated to reflect the new auth model.

## Fix

`check_status` now checks BOTH sources:

```rust
let has_legacy_api_key = config.api_key.as_ref().map_or(false, |k| !k.is_empty());
let has_session_jwt = crate::api::jwt::get_session_token(&config)
    .ok()
    .flatten()
    .is_some_and(|t| !t.is_empty());
let is_authenticated = has_legacy_api_key || has_session_jwt;
```

The status report now shows:
* `Authentication: configured ✓ (session token from desktop login)` —
  when the user has gone through the OAuth flow (the typical case);
* `Authentication: configured ✓ (legacy api_key)` — when the user
  has set the legacy `config.api_key` field directly (CI / dev
  setups);
* `Authentication: **missing** — log in via the desktop app or set
  `api_key` in config to enable inference` — when neither source has
  a credential.

The line label changed from "API key" to "Authentication" because
"API key" was misleading for both states (it isn't really the user's
API key; it's the openhuman backend session JWT).

## What this unblocks

After this fix, a Tauri user who:
1. Logs in once via the desktop OAuth flow → JWT lands in
   `auth-profiles.json`,
2. Types `hi` in the chat pane → welcome agent runs,
3. Welcome calls `check_status` → reports "Authentication: configured ✓",
4. Welcome calls `complete_onboarding(complete)` → flips
   `chat_onboarding_completed` from false to true,
5. Next chat turn → routes to orchestrator.

Without this fix, step 4 never happens because welcome's prompt
explicitly forbids completing without an API key, so the user gets
the welcome message on every single turn forever.

## Files touched

`src/openhuman/tools/impl/agent/complete_onboarding.rs`
* `check_status` reads both `config.api_key` and
  `crate::api::jwt::get_session_token(&config)`.
* Status line renamed from "API key" to "Authentication" with a
  more informative message.
* Long inline comment documenting the two auth sources, why the
  check needs to consult both, and what bug was being fixed.

## Tests

360/361 tools tests pass. Only failure is the pre-existing Windows
browser-screenshot bug (`tools::impl::browser::screenshot::
screenshot_command_contains_output_path`) which fails identically on
upstream/main and is unrelated to this PR. The
`complete_onboarding::tool_metadata` test still passes — it only
checks the tool schema, not the runtime behaviour.

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

* fix(welcome): force complete_onboarding(complete) in iteration 2 (#525)

Live Tauri test after Commit 11 (auth-aware check_status) showed the
welcome agent completing iteration 1 with the correct check_status
tool call AND iteration 2 with a beautiful 1586-char welcome message
— but NOT calling complete_onboarding(action="complete") to flip
chat_onboarding_completed. So the user gets the welcome message and
then routes to welcome AGAIN on every subsequent chat turn forever,
because the prompt's Step 3 is descriptive rather than imperative.

Same failure pattern as the iteration-1 chitchat fallback that
Commit 10 fixed — the LLM follows the path of least resistance. If
the prompt says "call X to finalize" without making it mandatory,
the model will write the welcome text, see that it's done its job,
and stop without emitting the second tool call.

Fix: rewrite Step 3 with the same "MANDATORY" preamble pattern that
worked for the iteration-1 fix. Specifically:

* Renamed Step 3 to "Complete onboarding — MANDATORY in iteration 2
  (when authenticated)" so the cross-reference is unmissable.
* Added a blockquoted "MANDATORY SECOND TOOL CALL" preamble at the
  top of Step 3 that:
  - States the rule: when check_status reports "Authentication:
    configured ✓", iteration 2 MUST contain BOTH the welcome message
    text AND a complete_onboarding(action="complete") tool call.
  - Explains the consequence: without the tool call, the user is
    routed to welcome forever, which is a hard failure.
  - Defines the iteration 2 output structure as a 2-element list:
    welcome text + tool call.
  - Shows / examples (welcome message without tool call vs. with
    tool call) so the model has a concrete pattern to match.
  - Documents the single exception: only when authentication is
    missing should complete() be skipped, and explicitly says
    "missing channels, missing Composio integrations, missing local
    AI — none of those block completion." Auth is the only blocker.
* Replaced the descriptive bullet list with a stricter "Decision
  rule for iteration 2" that pairs each check_status outcome with
  exactly what the agent must emit.

## What stays the same

* No code changes — the existing complete_onboarding tool already
  handles the action="complete" call correctly (Commit 11 made it
  flip the right flag and check the right auth source).
* No agent.toml changes — welcome still has 2 tools, max_iterations=6,
  temperature=0.7. Plenty of headroom for the 2-iteration flow.
* The decision logic itself is unchanged — auth → complete, no auth
  → no complete. Just the framing.

## Test plan

Restage sidecar, relaunch Tauri, type `hi`. Expected:

```
i=1: complete_onboarding(check_status) → "Authentication: configured ✓"
i=2: 1500-2000 char welcome message + complete_onboarding(action="complete")
       ↓
     [complete_onboarding] chat welcome flow marked complete, proactive agents seeded
       ↓
     chat_onboarding_completed flips false → true
```

Then type a follow-up message → expect:

```
[web-channel] route → orchestrator (chat_onboarding_completed=true)
```

That's the welcome → orchestrator handoff — the actual goal of #525.

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

* fix(web-channel): include target_agent_id in THREAD_SESSIONS cache key (#525)

Final bug discovered during E2E testing: after Commit 12 made the
welcome agent successfully call complete_onboarding(complete) and
flip chat_onboarding_completed to true, the user typed a follow-up
message — but the next turn STILL routed through the welcome agent
instead of orchestrator.

## Root cause

`run_chat_task` caches the built `Agent` in a `THREAD_SESSIONS`
HashMap keyed by `(client_id, thread_id)`. The cache hit predicate
checked `model_override` and `temperature` for invalidation but not
the routing target — so when the routing decision flipped
(chat_onboarding_completed: false → true) between turns, the cache
happily returned the stale welcome agent and `build_session_agent`
was never invoked.

This was a real bug introduced by Commit 8 — when I added
agent-aware routing to `build_session_agent`, I should have
extended the cache key (or hit predicate) with the target agent id.
Without that, the routing fix only worked on the FIRST turn of any
thread; every subsequent turn served the cached agent regardless of
flag changes.

Symptoms in the live test:
* Turn 1: routes to welcome ✓, welcome runs 3 iterations, calls
  complete(), flag flips on disk → cached as welcome agent
* Turn 2: cache hits on (client_id, thread_id), returns cached
  welcome agent. NO `[web-channel]` routing trace, NO
  `[agent::builder]` trace. Welcome runs again with history_len=10.
* Result: orchestrator handoff never happens. User stuck in welcome
  forever.

## Fix

Three small changes in `web.rs`:

### 1. `SessionEntry` gains a `target_agent_id` field

```rust
struct SessionEntry {
    agent: Agent,
    model_override: Option<String>,
    temperature: Option<f64>,
    target_agent_id: String,  // NEW
}
```

Documents which agent definition was used to build the cached
`Agent`, so the next turn's cache lookup can compare against the
current routing decision.

### 2. New `pick_target_agent_id(config)` helper

```rust
fn pick_target_agent_id(config: &Config) -> &'static str {
    if config.chat_onboarding_completed {
        "orchestrator"
    } else {
        "welcome"
    }
}
```

Mirrors the routing decision inside `build_session_agent` so
`run_chat_task` can compute it once up front and use it as a cache
key component. Since `Config::load_or_init` reads from disk every
call, the value reflects the freshly persisted state — meaning the
moment the welcome agent flips the flag, the next turn's
`pick_target_agent_id` returns the new value, the cache hit
predicate falls through, and `build_session_agent` is invoked.

### 3. Cache hit predicate extended

```rust
let mut agent = match prior {
    Some(entry)
        if entry.model_override == model_override
            && entry.temperature == temperature
            && entry.target_agent_id == target_agent_id =>
    {
        log::info!("[web-channel] reusing cached session agent id={} ...");
        entry.agent
    }
    Some(prior_entry) => {
        log::info!(
            "[web-channel] cache miss — rebuilding session agent \
             (was id={}, now id={}) ...",
            prior_entry.target_agent_id, target_agent_id
        );
        build_session_agent(...)?
    }
    None => build_session_agent(...)?,
};
```

The `Some(prior_entry)` arm now distinguishes stale-cache hits
(rebuild + log the transition) from cold misses (rebuild silently).
The transition log line gives observability for the welcome →
orchestrator handoff: whenever you see "cache miss — rebuilding
session agent (was id=welcome, now id=orchestrator)", that's the
moment of the handoff in production.

## Tests

Library compiles. The cache-key change is small and the logic is
straightforward; no test changes needed (existing tests don't
exercise the `THREAD_SESSIONS` cache flow, and adding a unit test
would require mocking the global config + memory backend which is
significantly more setup than the change itself warrants).

Live verification: type `hi` (welcome), wait for the "all set"
message + flag flip, type a follow-up. Expected logs:

```
[web-channel] routing chat turn to 'welcome'  (turn 1)
[agent::builder] building session agent id=welcome
... welcome runs, calls complete(), flag flips ...

[web-channel] routing chat turn to 'orchestrator'  (turn 2)
[web-channel] cache miss — rebuilding session agent (was id=welcome, now id=orchestrator)
[agent::builder] building session agent id=orchestrator
```

That's the complete welcome → orchestrator handoff that has been
the goal of #525 since day one.

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

* fix(complete_onboarding): document tool semantics in schema, terse success result (#525)

Stopping the prompt-bloat cycle on the welcome agent. Previous
commits (10, 12, and a half-applied iteration of 14) progressively
added MANDATORY blockquotes to welcome's prompt.md to fix three
distinct failure modes: chitchat-fallback in iter 1, missing
complete() in iter 2, and prose-leak in iter 3. Each fix worked but
the welcome prompt is now ~250 lines of imperatives encoding what
should be tool semantics, not agent persona.

The right home for tool semantics is the tool's own schema —
specifically `Tool::description()` and `parameters_schema()`. Those
fields are what the LLM sees when deciding when and how to call the
tool, and they apply to every agent that uses the tool, not just
welcome. Encoding the contract there lets the welcome prompt stay
about persona / workflow / tone, not implementation details.

## Changes

### `tools/impl/agent/complete_onboarding.rs`

#### `Tool::description()` — full rewrite

Replaces the previous 5-line description with a structured ~30-line
contract documenting BOTH actions:

* **`check_status`** — explicit list of what the report contains
  (auth, default model, channels, integrations, memory, both
  onboarding flags), explicit "side effects: NONE (read-only)"
  declaration, and explicit framing as "intended for an LLM agent
  to read and use as the basis for a personalized welcome message"
  so consumers know how to use the result.

* **`complete`** — explicit semantics: flips
  `chat_onboarding_completed`, triggers welcome→orchestrator
  handoff, seeds proactive cron jobs, idempotent, writes config.toml,
  has a pre-condition (must be authenticated). And critically:

  > The complete action returns the literal token "ok" on success.
  > **This return value is a machine-readable success marker, not
  > user-facing prose.** Do not paraphrase it, summarize it, or
  > acknowledge it back to the user — the actual user-facing welcome
  > text should have been emitted alongside the tool call in the
  > same iteration. The chat layer extracts the LAST iteration's
  > text as the user-visible reply, so any prose written after this
  > tool returns will overwrite the welcome message in the chat pane.

  This is the contract that prevents the iter-3 paraphrase leak,
  documented at the source of the tool result instead of in every
  consumer's prompt.

#### `parameters_schema()` — extended action description

The `action` enum's description now mirrors the contract:
> "check_status" → read-only inspection ... "complete" → finalize
> the chat welcome flow, flips chat_onboarding_completed to true,
> returns the literal token "ok" (NOT a user-facing message — do
> not paraphrase the result back to the user).

JSON-schema descriptions are seen by the LLM at function-calling
decision time, so the warning lands in the same place the LLM is
deciding whether to call the tool.

#### `complete()` return value

Changed from a 118-char chatty success string ("Chat welcome flow
marked as complete. Morning briefing and proactive agent jobs have
been set up. The user is all set!") to the literal 2-char `"ok"`.

The chatty string was the source of the iter-3 paraphrase leak —
the LLM kept treating it as something to summarize back to the user
in iteration 3, which overwrote the iter-2 welcome message in the
chat pane (because the chat layer extracts the last iteration's
text). With "ok" there's nothing to paraphrase.

The inline comment block on the return statement records the bug
history so a future maintainer who sees `Ok(ToolResult::success("ok"))`
and is tempted to make it more "informative" understands why it's
deliberately terse.

### `agents/welcome/prompt.md`

Reverts the half-applied "Step 6: STOP after iteration 2" blockquote
that I started adding in the previous commit attempt. Replaces it
with a single one-line pointer at the end of Step 5:

> "(See the `complete_onboarding` tool's own description for what
> its `"ok"` return value means and why you should not paraphrase it
> back to the user.)"

That's enough context for the welcome agent to understand the
return-value contract without duplicating the contract text. The
contract lives in the tool, not in the agent that consumes it.

## What this DOESN'T touch

* Commits 10 (mandatory first action) and 12 (mandatory second tool
  call) stay as-is. They're still arguably too forceful, but they
  encode workflow steps specific to the welcome agent's persona,
  not general tool semantics. Trimming them is a separate prompt
  audit follow-up.

* No code changes outside `complete_onboarding.rs`. No agent.toml
  changes, no schema changes, no other tool changes. Pure
  documentation + return-value tweak.

## Tests

`tool_metadata` still passes (it pins name, scope, permission_level,
and schema shape — not description text, which is intentionally
allowed to drift). 1/1 complete_onboarding tests pass. Library
compiles clean.

## Test plan

Restage sidecar, relaunch Tauri, type `hi`. Expected:
* iter 1: check_status → 600-char status report
* iter 2: 1500-char welcome message + complete_onboarding(complete)
* iter 3 (if it fires): no prose, or trivial prose ≪ iter 2 chars
* Chat pane shows the iter-2 welcome message, NOT a confirmation
  paraphrase.

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

* fix(complete_onboarding): merge check + finalize, return JSON, halve welcome prompt (#525)

End-to-end testing surfaced the cleanest version of this design,
collapsing what had grown into a multi-iteration imperative dance
back into a single tool call with a JSON return value. Three
problems addressed in one pass:

## Problem 1 — two tool calls were a state-machine race condition

Previous design required the welcome agent to call two tools in
order: `complete_onboarding(check_status)` in iteration 1, then
`complete_onboarding(complete)` in iteration 2 alongside the welcome
text. This created a fragile state machine: if the model wrote the
welcome message but forgot the second tool call, the chat_onboarding
flag never flipped and the user got stuck in welcome forever. The
prompt grew a 50-line "MANDATORY SECOND TOOL CALL" blockquote with
multiple / examples to compensate for what was really an API
shape problem.

## Problem 2 — Markdown report fed paraphrase loops

`check_status` returned a 600-char human-readable Markdown report
with section headers, bullet points, and check marks. The LLM kept
treating it as a draft and paraphrasing fragments back into the
welcome message instead of using the field values as ground-truth
facts. Worse, the structured-looking output gave the model
permission to "wrap up" the tool result in iteration 3, producing
the (parenthetical) leak text the user kept seeing.

## Problem 3 — prompt was bloated past the point of usefulness

After three rounds of "the LLM did the wrong thing, add a more
forceful blockquote", the welcome prompt was ~250 lines, with
overlapping MANDATORY blocks, repeated / examples, and
imperatives that contradicted the tone guidelines. A model trying
to serve a coherent welcome persona could not emerge from that.

## Fix — single call, JSON return, auto-finalize as a side effect

`check_status` now does all of:

1. Reads the user's config.
2. Checks JWT via `crate::api::jwt::get_session_token` (the auth
   source the rest of the codebase uses).
3. **If authenticated AND chat_onboarding_completed is false, flips
   the flag + seeds proactive cron jobs as a side effect**. No
   second tool call, no parameter, no opt-in. Authentication is the
   only gate.
4. Returns a structured JSON snapshot:
   ```
   {
     "authenticated": true,
     "auth_source": "session_token",
     "default_model": "reasoning-v1",
     "channels_connected": ["telegram"],
     "active_channel": "web",
     "integrations": {
       "composio": false,
       "browser": false,
       "web_search": true,
       "http_request": false,
       "local_ai": true
     },
     "memory": { "backend": "sqlite", "auto_save": true },
     "delegate_agents": [],
     "ui_onboarding_completed": true,
     "chat_onboarding_completed": true,
     "finalize_action": "flipped"
   }
   ```

The `finalize_action` field discriminates three cases the welcome
agent needs to handle differently:

* `"flipped"` — auth ok, first welcome, flag was just flipped by
  this call. Write the full welcome and hand off.
* `"already_complete"` — auth ok, chat flow already done from a
  prior call. Friendly re-entry; still write a welcome but
  acknowledge they've been here before.
* `"skipped_no_auth"` — not authenticated. Don't write a celebratory
  welcome; explain the auth problem and let them retry on the next
  chat turn.

The `complete` action stays as a legacy manual finalize-only escape
hatch for admin tools / tests. Returns "ok". Welcome agent doesn't
use it.

## Welcome prompt rewrite

Down from ~250 lines to ~140. Specifically removed:

* "MANDATORY FIRST ACTION" blockquote (40 lines) — replaced with
  one tight paragraph in the new "Iteration 1: call
  complete_onboarding" section.
* "MANDATORY SECOND TOOL CALL" blockquote (50 lines) — entirely
  gone. There is no second tool call.
* "STOP after iteration 2" Step 6 from a previous failed iteration
  (never landed in this branch but the structure that motivated it
  is gone too).
* "Decision rule for iteration 2" (5 lines) — replaced with a
  three-bullet list keyed off `finalize_action`.
* Three duplicate "Don't reply with a 1-line greeting" / "Don't
  treat 'hi' as a signal to be brief" rules — collapsed into one
  shorter rule.

Added:

* "You have exactly two iterations. There is no third." — single
  declarative sentence that does the work the previous 90 lines of
  iteration imperatives were doing.
* Explicit framing that JSON is a fact source, not a draft. "Don't
  quote, paraphrase, or summarise the JSON snapshot back to the
  user."
* `finalize_action` decision tree (three bullets) so the agent
  knows what message to write for each case without the prompt
  needing to enumerate side effects.
* Handling for `skipped_no_auth` — short, helpful "you need to log
  in first" message instead of the celebratory welcome.

## Why this works without any iteration-2 imperatives

With a single tool call, the agent loop converges naturally:

* iter 1: tool call (the only thing the prompt allows)
* tool returns: JSON snapshot, flag already flipped server-side
* iter 2: prose response (no remaining tool calls to make)
* iter 3: doesn't fire because there's no incomplete work — the
  loop terminator at `turn.rs:351` returns when `calls.is_empty()`,
  which is iter 2's state.

No race, no forgotten flip, no paraphrased tool result, no
parenthetical leak.

## Files

`src/openhuman/tools/impl/agent/complete_onboarding.rs` — full
rewrite. New `check_status` builds the JSON, performs the auto-
finalize side effect, returns serialised JSON via `ToolResult::
success`. Old Markdown-report code deleted (~150 lines). Tool
description and `parameters_schema` rewritten to document the new
contract; the `finalize` parameter from the previous attempt is
removed entirely (auth presence IS the gate).

`src/openhuman/agent/agents/welcome/prompt.md` — full rewrite.
~250 → ~140 lines. New "two iterations, no third" workflow.
JSON-aware drafting instructions. `finalize_action` decision tree.

## Tests

* `tool_metadata` (1/1): pins schema shape (action enum, required
  fields). Still passes — schema unchanged at the level the test
  asserts.
* `agent::agents::*` (14/14): all welcome-definition shape tests
  pass. The TOML hasn't changed, only the embedded `prompt.md`.

## Verification

Restage sidecar, relaunch Tauri, type `hi`. Expected:

* iter 1: `complete_onboarding(check_status)` → JSON returned with
  `finalize_action: "flipped"`. Side effect: chat_onboarding
  flipped to true on disk.
* iter 2: ~250-word welcome message based on the JSON.
* iter 3: not reached. Loop terminates after iter 2.
* Chat pane shows the iter-2 welcome. No paraphrase leak.

Then type a follow-up:

* `[web-channel] cache miss — rebuilding session agent (was
  id=welcome, now id=orchestrator)` — the welcome → orchestrator
  handoff fires via Commit 13's cache invalidation.

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

* fix(welcome): hide the welcome→orchestrator handoff from the user (#525)

The welcome agent's prompt previously instructed it to close every
welcome message with an explicit handoff line:

> "From here, you're in the hands of the full OpenHuman assistant.
>  Just start a new conversation and ask it anything — it knows how
>  to delegate to specialists, run tools, search the web..."

That line is a UX leak. From the user's perspective they are
talking to "OpenHuman" — one assistant, one conversation. They do
not know there are multiple agents under the hood, and they
shouldn't have to. The welcome → orchestrator transition is a
routing-layer implementation detail. Surfacing it in the chat
makes openhuman feel like a chatbot framework instead of a
product.

## Changes (welcome prompt only — no code)

### Step 5 of the message structure

Renamed from "Hand off" to "Close naturally". New instruction:

> "End the message with something inviting like 'anything you'd
>  like to try first?' or 'what should we dig into?' — a normal
>  conversational close that lets the user pick up the next turn.
>  Do NOT mention handing off, transferring, or any change in who
>  they're talking to. The user does not know there are multiple
>  agents under the hood; from their perspective they're talking
>  to OpenHuman as one entity, and the next message they send
>  will just continue the conversation."

### "What NOT to do" list

Replaced the old "Don't forget the handoff" rule with its inverse:

> "**Don't reveal that the conversation is being routed to a
> different agent.** From the user's perspective they are talking
> to 'OpenHuman' — one assistant, one conversation. Do NOT say
> 'I'll hand you off to the main assistant', 'the orchestrator
> will take over', 'you're now in the hands of the full assistant',
> 'from here on out you'll be talking to a different agent', or
> any variation that exposes the welcome → orchestrator handoff.
> The handoff happens transparently in the routing layer; the user
> just sends another message and the conversation continues.
> Phrases like 'what should we dig into?' or 'anything you'd like
> to try first?' are correct conversational closes — they invite
> the next turn without leaking the architecture."

The rule lists four specific forbidden phrasings the previous
welcome agent runs produced (variations of "you're in the hands
of the full assistant" and "the orchestrator will take over") so
the model has explicit anti-patterns to match against, plus two
acceptable conversational closes.

## Why this matters for #525

The welcome → orchestrator handoff is the central architectural
contribution of this PR. From a developer's perspective it is a
big deal — different agent definition, different tool scope,
different prompt, different temperature, different model hint.
From the user's perspective it should be invisible. They type a
message, OpenHuman replies. They type another, OpenHuman replies.
The fact that one of those replies came from `welcome` and the
next from `orchestrator` is purely an implementation detail they
should never have to think about.

## What does NOT change

* No code changes. Pure prompt edit.
* The handoff still happens — `chat_onboarding_completed` still
  flips to true via the `check_status` auto-finalize, the
  `THREAD_SESSIONS` cache still invalidates between turns (Commit
  13), the next chat turn still routes to orchestrator (Commit 8).
  The mechanism is unchanged; only the user-facing framing of it
  is.
* No test changes. `agent::agents::welcome_has_onboarding_and_
  memory_tools` and `every_builtin_has_a_prompt_body` only
  validate the agent definition shape, not the prompt body text,
  so this commit does not affect them.

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

* refactor(complete_onboarding): delegate auto-finalize side effect to complete() (#525)

Small refactor on top of Commit 15. The auto-finalize block inside
`check_status` previously inlined the entire flag-flip + cron-seed
sequence:

```rust
config.chat_onboarding_completed = true;
config.save().await?;
let seed_config = config.clone();
tokio::spawn(async move {
    if let Err(e) = crate::openhuman::cron::seed::seed_proactive_agents(&seed_config) {
        tracing::warn!("...");
    }
});
```

But the legacy `complete()` action does literally the same work.
Duplicating it meant two places to update if the finalize semantics
ever changed (e.g. add another side effect, change the cron seed
strategy). Refactor `check_status` to delegate to `complete()`:

```rust
let _ = complete().await?;
config.chat_onboarding_completed = true;  // mirror disk into local
```

The `let _` discards `complete()`'s `ToolResult::success("ok")`
return value — `check_status` is producing its own JSON snapshot,
the caller just wants the side effect.

The `config.chat_onboarding_completed = true` after the call is an
in-memory mirror of the flip that `complete()` just performed on
disk. Without it, the JSON snapshot built later in `check_status`
would still show the pre-flip value, because the local `config` was
loaded BEFORE `complete()` ran and `complete()` operates on its own
fresh disk-loaded copy. The mirror is one line, no extra disk read,
no behavior change.

## Why not extract a `perform_finalize(&mut Config)` helper instead

Considered. Would eliminate the in-memory mirror and the redundant
disk read inside `complete()`. Ruled out because:

* `complete()` is already documented and used as a public legacy
  action; changing its signature would ripple to admin tools and
  tests that call it through the action dispatcher.
* The mirror cost is one line, zero I/O, zero correctness risk.
* The literal "call complete() inside check_status" framing matches
  the request more directly.

If a future maintainer wants to refactor further, the obvious move
is to make `complete()` take `&mut Config`, drop the redundant
`Config::load_or_init` inside `complete()`, and have both
`check_status` and the public action handler load the config once
at their top level. That's a separate refactor.

## Files

`src/openhuman/tools/impl/agent/complete_onboarding.rs` — single
function body change in `check_status`. The "flipped" arm of the
`finalize_action` match now calls `complete()` and mirrors the
flip locally instead of inlining the flip + save + cron seed.
About -10 / +5 lines.

## Tests

Library compiles. The 1 `tool_metadata` test still passes (it
pins the schema shape, not function bodies). No behavior change
for any caller.

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

* style(builder): cargo fmt cleanup of from_config_for_agent body (#525)

Pure indentation cleanup applied by `cargo fmt` against the
`target_def` resolution block I added in Commit 8 (`feat(web-channel):
route Tauri in-app chat to welcome/orchestrator`). The original
indentation had an awkward type annotation on the `let target_def:
Option<...> = ` line that wrapped before the `match` keyword;
rustfmt prefers the `=` on the same line as the type and the
`match` body indented under it.

Zero behavior change. Same control flow, same logic, same
diagnostics. Detected by the husky pre-push hook running
`yarn rust:check` (which runs `cargo fmt --check`).

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

* test(json_rpc_e2e): set chat_onboarding_completed=true in seed config (#525)

The e2e config seeded by `write_min_config` now sets
`chat_onboarding_completed = true` so `channel_web_chat` routes straight
to the orchestrator. Without this the first chat turn goes through the
new welcome agent whose tool contract is not modelled by the in-process
mock upstream, which tore down the SSE stream mid-response and caused
`json_rpc_protocol_auth_and_agent_hello` to fail with
`sse stream read failed: error decoding response body`.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
2026-04-13 14:17:11 -07:00
Steven EnamakelandGitHub 8057f2283c feat: onboarding Gmail integration + LinkedIn profile enrichment (#524)
* refactor(composio): restructure toolkit metadata handling and onboarding steps

- Removed the old `toolkitMeta.ts` file and replaced it with a new `toolkitMeta.tsx` file that includes updated metadata handling for Composio toolkits, enhancing the integration with React components.
- Updated the `ComposioConnectModal` to directly render icons without additional markup, streamlining the component structure.
- Modified the `Skills` page to utilize the new icon rendering method, improving consistency across the application.
- Enhanced the onboarding process by introducing a new `ContextGatheringStep` component, which gathers user context from connected integrations, improving the onboarding experience.
- Updated the `SkillsStep` to reflect changes in toolkit connection handling and display, ensuring a smoother user interaction during onboarding.

* feat(apify): introduce Apify integration tools for actor execution and status retrieval

- Added new tools for running Apify actors and fetching their run statuses, enhancing automation capabilities.
- Updated the integration schema to include an `apify` toggle for user configuration, allowing for flexible integration management.
- Enhanced the onboarding experience by modifying the SkillsStep to focus on Gmail integration, streamlining user interactions.
- Improved documentation and comments for clarity on the new Apify functionalities and their usage.

* feat(learning): add LinkedIn enrichment module and schemas

- Introduced a new `linkedin_enrichment` module for enriching user profiles by scraping LinkedIn data from Gmail.
- Implemented the `run_linkedin_enrichment` function to handle the enrichment pipeline, including Gmail search, scraping via Apify, and data persistence.
- Added controller schemas for the learning domain, enabling integration with the existing controller framework.
- Updated the `all.rs` file to register the new learning controllers and schemas, enhancing the overall functionality of the learning system.

* feat(linkedin): implement PROFILE.md generation for LinkedIn enrichment

- Added functionality to generate a PROFILE.md file from scraped LinkedIn data, summarizing user profiles for agent context.
- Updated the `run_linkedin_enrichment` function to write PROFILE.md to the workspace, enhancing data persistence.
- Introduced helper functions for rendering and summarizing LinkedIn profiles, improving the overall enrichment process.
- Ensured minimal PROFILE.md creation even when scraping fails, maintaining essential user context.

* feat(onboarding): enhance ContextGatheringStep for LinkedIn enrichment pipeline

- Updated the ContextGatheringStep to integrate a new pipeline for LinkedIn enrichment, replacing the previous Gmail profile fetching stages.
- Implemented a progress animation and logging for the enrichment process, improving user feedback during data retrieval.
- Refactored stage definitions to align with the new pipeline structure, enhancing clarity and maintainability.
- Introduced error handling and status updates for each stage of the enrichment process, ensuring robust user experience.

* feat(instructions): refactor tool instruction generation for clarity and flexibility

- Introduced helper functions `tool_instructions_preamble` and `append_tool_entry` to streamline the construction of tool instructions.
- Updated `build_tool_instructions` to utilize the new helper functions, improving code readability and maintainability.
- Added `build_tool_instructions_filtered` to allow for generating instructions from a filtered list of tools, enhancing flexibility in tool usage.
- Adjusted the startup process to use the filtered instructions, ensuring only relevant tools are included in the system prompt.

* refactor(toolkitMeta): simplify component structure and improve readability

- Refactored the `BrandIcon` component to streamline its props definition, enhancing code clarity.
- Consolidated SVG path definitions in various icons for better readability and maintainability.
- Updated the `ContextGatheringStep` to simplify the RPC call syntax, improving code conciseness.
- Enhanced logging in the LinkedIn enrichment process for clearer tracking of Gmail searches and scraping stages.

* fix: address PR review — USER.md→PROFILE.md consistency, Composio tool filtering, and quality fixes

- Replace USER.md with PROFILE.md across all prompt paths: channels_prompt.rs,
  subconscious/prompt.rs, workspace/ops.rs bootstrap, and channel tests
- Remove Composio tool description from main agent system prompt (tool_descs)
  so skills_agent is the only agent that sees Skill-category tools
- Add "learning" namespace description for CLI help discovery
- Fix react-hooks/set-state-in-effect: wrap synchronous setState in
  queueMicrotask in ContextGatheringStep
- Derive SkillsStep displayToolkits from backend allowlist with error/retry UI
- Add KNOWN_COMPOSIO_TOOLKITS alternate slug variants (google_calendar, etc.)
- Add namespaced debug logging to onboarding handlers and pipeline
- Deduplicate MemoryClient creation in linkedin_enrichment persist functions

* fix: use setTimeout instead of queueMicrotask for ESLint compatibility

* refactor(onboarding): streamline debug logging in handleContextNext function

- Consolidated debug logging in the handleContextNext function to improve clarity and reduce verbosity.
- Removed unnecessary line breaks for a more concise code structure.

* refactor(linkedin): enhance enrichment pipeline with structured stage results

- Introduced a new `EnrichmentStage` struct to capture detailed results for each stage of the LinkedIn enrichment process.
- Updated the `LinkedInEnrichmentResult` to include a vector of stages, allowing for structured reporting of success, failure, and skipped stages.
- Improved error handling and logging throughout the enrichment pipeline, ensuring better traceability of issues during execution.
- Adjusted the API response to include stage results, enhancing the frontend's ability to display detailed enrichment outcomes.

* chore(workflows): update macOS E2E test configuration and comment out Linux E2E job

- Modified the description and default values in the macOS E2E test input options for clarity.
- Commented out the entire Linux E2E job configuration to prevent execution while maintaining the setup for future use.
2026-04-13 14:16:53 -07:00
08d9fd2d4d fix(voice): recover buffered hotkey events after select! race (#527) (#545)
* fix(voice): return receiver count from publish_transcription

publish_transcription now returns the number of active TRANSCRIPTION_BUS
subscribers that received the message. When no receivers are connected
(e.g. Socket.IO bridge not yet subscribed), the function logs a warning
instead of silently discarding the broadcast.

This makes it possible to diagnose "transcription produced but never
delivered" scenarios from logs alone.

Closes #527 (partial)

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

* fix(voice): recover buffered hotkey events after select! race

On warm CPAL init (second+ recording), audio_capture::start_recording()
completes fast enough that the pending_ready branch and the hotkey_rx
branch are both ready simultaneously inside tokio::select!. select!
picks one pseudo-randomly — when it picks pending_ready, the Released
event sits unprocessed in hotkey_rx. The recording is stored as "live"
with no deferred stop, then the next loop iteration processes the
buffered Released and stops the recording almost immediately, producing
a near-zero-length clip that the duration gate silently drops.

Fix: after pending_ready resolves, call hotkey_rx.try_recv() to check
for a buffered stop event that lost the race. If found, apply the
deferred stop mechanism (MIN_RECORDING_AFTER_SETUP = 1500ms) instead
of treating the recording as live.

Also adds pipeline_id (UUID prefix) to all process_recording_bg log
lines for end-to-end correlation, and labels each pipeline stage
(stop_recording, gate_duration, gate_silence, transcribe, deliver)
so dropped recordings are diagnosable from logs.

Closes #527

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 14:07:28 -07:00
Steven EnamakelandGitHub ec138c8491 feat(composio): provider folder modules + user profile persistence (#523)
* feat(composio): implement profile persistence for user data

- Introduced a new `profile` module to handle the persistence of user profile data from various providers into the local `user_profile` facet table.
- Enhanced the `composio_get_user_profile` and `fetch_user_profile` functions to call `persist_provider_profile`, ensuring that profile fields like display name, email, and avatar are stored locally for quick access.
- Added debug logging to track the number of facets written during the persistence process, improving observability of profile updates.
- This change aims to enhance user experience by reducing the need for repeated upstream API calls for frequently accessed profile information.

* style: apply cargo fmt to profile.rs and client.rs

* fix: PR review — cursor whitespace, stale profile values, test precision, log level

- Trim whitespace in cursor_to_gmail_after_filter before parsing so
  leading/trailing spaces don't silently bypass the date filter.
- Change profile_upsert condition from `>` to `>=` so equal-confidence
  provider refreshes replace stale values instead of only bumping
  evidence_count.
- Tighten epoch millis test to assert exact date "2026/03/31" instead
  of loose contains('/') check.
- Lower profile persistence log from info to debug to reduce noise in
  normal connect/sync flows.
2026-04-12 19:18:07 -07:00
Steven EnamakelandGitHub ae9ac30db6 feat: add proactive welcome onboarding flow (#522)
* feat: add proactive agents for onboarding experience

- Introduced two new proactive agents: "Morning Briefing" and "Welcome Message".
- The Morning Briefing agent provides a daily summary of the user's calendar, tasks, and relevant context, scheduled to run automatically at 7 AM.
- The Welcome Message agent delivers a personalized greeting after onboarding, utilizing user information for a tailored experience.
- Updated the cron job system to seed these agents upon onboarding completion, enhancing user engagement from the start.
- Added comprehensive tests to ensure the correct functionality of the new agents and their integration into the system.

* feat: implement proactive message handling for user engagement

- Added a new `ProactiveMessageSubscriber` to route proactive messages (e.g., morning briefings, welcome messages) to the user's active channel, enhancing user engagement.
- Introduced `ProactiveMessageRequested` event in the event bus to facilitate proactive message delivery.
- Updated the configuration schema to include a preferred channel for proactive messages, allowing for tailored user experiences.
- Seeded default proactive agent cron jobs to ensure timely delivery of messages post-onboarding.
- Enhanced the startup process to register the proactive message subscriber, ensuring proactive messages are consistently routed.
- Comprehensive tests added to validate the functionality of proactive message handling and delivery mechanisms.

* feat: enhance welcome agent functionality and onboarding process

- Updated the welcome agent's description to clarify its role as the first point of contact for new users, focusing on guiding them through remaining onboarding steps.
- Increased the maximum iterations for the agent from 4 to 6 to allow for more complex interactions.
- Introduced a new `complete_onboarding` tool to check the user's setup status and mark onboarding as complete, improving the onboarding experience.
- Enhanced the prompt structure to provide clearer guidance on the agent's workflow, including steps for checking setup status, greeting users, and completing onboarding.
- Updated the tools list to include `complete_onboarding`, ensuring the welcome agent can effectively manage user onboarding.
- Comprehensive documentation added to clarify the agent's functionality and improve user engagement.

* feat: enhance welcome agent functionality and onboarding process

- Updated the welcome agent's description to clarify its role as the first point of contact for new users, focusing on guiding them through setup and onboarding.
- Increased the maximum iterations for the agent from 4 to 6 to allow for more comprehensive interactions.
- Introduced a new `complete_onboarding` tool to check the user's setup status and mark onboarding as complete, improving the onboarding experience.
- Enhanced the prompt structure to provide clearer guidance on the agent's workflow, including steps for checking setup status, greeting users, and completing onboarding.
- Updated the tools list to include the new `complete_onboarding` tool, ensuring it is available for the welcome agent's operations.
- Improved documentation for the welcome agent's prompt, emphasizing tone guidelines and what not to do during user interactions.

* style: apply rustfmt to onboarding flow changes

* refactor: streamline proactive message subscriber registration and enhance onboarding agent tests

- Replaced the direct registration of the proactive message subscriber with a new `register_web_only_proactive_subscriber` function, ensuring it is only registered once during domain startup.
- Updated the welcome agent test to reflect the addition of the `complete_onboarding` tool, verifying its presence and adjusting the expected number of tools.
- Enhanced the onboarding process by improving logging for proactive agent seeding transitions, providing clearer insights during onboarding state changes.
- Refactored the cron job system to support agent definitions, allowing for more flexible job scheduling and execution based on defined agent parameters.
- Improved documentation and comments across the affected modules to clarify the purpose and functionality of changes made.

* refactor: improve proactive subscriber registration and logging

- Streamlined the registration process for the web-only proactive message subscriber, enhancing clarity and maintainability.
- Consolidated logging statements in the onboarding completion function for improved readability and consistency.
- These changes aim to enhance the overall structure and logging clarity within the proactive messaging and onboarding processes.
2026-04-12 19:15:42 -07:00
Steven EnamakelandGitHub 4cf608c2be feat: native embeddings module with Ollama and vector store (#521)
* feat: replace fastembed with candle for local embeddings

- Introduced a new `CandleEmbedding` provider using the `candle` ML framework, eliminating C++ dependencies and enhancing performance.
- Updated configuration to default to `candle` for embedding provider settings, replacing the previous `fastembed` references.
- Added new modules for embedding providers, including `candle_embed`, `noop`, and `openai`, to support various embedding strategies.
- Enhanced the `EmbeddingProvider` interface to accommodate the new `CandleEmbedding` implementation.
- Refactored related tests to ensure comprehensive coverage of the new embedding functionalities and maintain backward compatibility with existing configurations.

* feat: update embedding provider to Ollama

- Replaced the default embedding provider from Candle to Ollama, enhancing local embedding capabilities with improved model management and GPU acceleration.
- Updated configuration defaults for embedding model and dimensions to align with Ollama specifications.
- Introduced a new Ollama embedding module, including necessary constants and functionality for embedding requests.
- Refactored related code and tests to ensure compatibility with the new provider, maintaining backward compatibility with existing configurations.

* refactor: remove Candle embedding provider and related dependencies

- Deleted the `CandleEmbedding` module and its associated files, streamlining the embedding provider architecture.
- Updated `Cargo.toml` and `Cargo.lock` to remove references to Candle-related packages, ensuring a cleaner dependency tree.
- Refactored the `EmbeddingProvider` interface to eliminate support for the Candle provider, maintaining compatibility with existing providers.
- Adjusted tests and documentation to reflect the removal of the Candle embedding functionality, ensuring clarity and consistency across the codebase.

* feat: add SQLite-backed vector store for embeddings

- Introduced a new `store` module to implement a local vector store backed by SQLite, enabling efficient storage and retrieval of text embeddings.
- Added functionality for inserting, updating, and searching embeddings using cosine similarity, enhancing the embedding management capabilities.
- Updated the `mod.rs` file to include the new `store` module and expose relevant functions for cosine similarity and vector operations.
- Enhanced documentation to provide usage examples and clarify the integration of the vector store with existing embedding providers.

* Add fs2 dependency for improved file locking in ComposeIO trigger history

* test: boost embeddings module coverage to 97%+

Add tests for batch insert mismatch error path, invalid metadata
JSON handling, disk store parent directory creation, and fake
embedding dimensions accessor. 95 tests total across the module,
all files at 97-100% line coverage.

* style: apply cargo fmt to embeddings module

* feat: enhance embedding provider creation with error handling

- Updated the `create_embedding_provider` function to return an `anyhow::Result`, allowing for immediate error reporting on unrecognized provider names.
- Added support for a "none" provider that returns a no-op embedding.
- Enhanced tests to cover new error handling paths and validate behavior for known and unknown providers, improving overall test coverage.

* feat: enhance Ollama and OpenAI embedding providers with improved handling for blank inputs and response validation

- Updated the `embed` method in both `OllamaEmbedding` and `OpenAiEmbedding` to skip blank inputs while preserving their positions in the output as zero-vectors.
- Added validation to ensure the response count matches the input count, with appropriate error handling for dimension mismatches.
- Enhanced tests to cover new behavior for blank inputs and response validation, ensuring robustness in embedding functionality.

* fix: address PR review findings for embeddings module

- Factory returns Result for unknown providers instead of silent noop
- Ollama: preserve positional alignment when blank texts are filtered
- Ollama: validate response count and dimensions before returning
- OpenAI: strict parsing errors on non-numeric embedding values
- OpenAI: skip Authorization header when api_key is empty
- OpenAI: validate response count and dimensions
- OpenAI/Ollama: add tracing at entry, success, and error paths
- Store: add store_meta table to persist and validate embedding
  provider/dimensions on open — errors on dimension mismatch
- Store: propagate row decode errors instead of filter_map(ok)
- Store: add tracing to search, insert, delete, and count paths
- Update factories.rs caller for Result-returning factory

101 tests pass, all files at 97%+ coverage.
2026-04-12 18:54:38 -07:00