docs: remove stale plan/status docs and clean dangling references (#4508)

This commit is contained in:
Steven Enamakel
2026-07-04 11:55:50 -07:00
committed by GitHub
parent d0f0b6efeb
commit a968f72b2b
91 changed files with 8 additions and 18059 deletions
+2 -9
View File
@@ -198,7 +198,7 @@ cargo check --manifest-path Cargo.toml
cargo check --manifest-path app/src-tauri/Cargo.toml
```
If you only changed docs in a normal local workflow, `pnpm format:check` is usually the only validation you need. AI-authored or remote-agent PRs must still follow [`docs/agent-workflows/codex-pr-checklist.md`](docs/agent-workflows/codex-pr-checklist.md) and report any blocked commands with the exact command and error.
If you only changed docs in a normal local workflow, `pnpm format:check` is usually the only validation you need. AI-authored or remote-agent PRs must still fill in the AI Authored PR Metadata section of the PR template and report any blocked commands with the exact command and error.
### 6. Run tests and checks
@@ -217,7 +217,7 @@ Merge-gate context:
- PRs must meet the checks enforced by CI and keep changed-line coverage at or above 80%.
- For code changes, run the smallest relevant local checks before you push.
- For AI-authored or remote-agent PRs, also follow [`docs/agent-workflows/codex-pr-checklist.md`](docs/agent-workflows/codex-pr-checklist.md).
- For AI-authored or remote-agent PRs, also fill in the AI Authored PR Metadata section of the PR template.
### 7. Local data and user-facing state
@@ -302,13 +302,6 @@ git checkout -b docs/your-change
If you are contributing through a coding agent or remote environment, include the metadata required by the PR template and the Codex PR checklist.
### Contributor rewards
Maintainers can reward eligible contributors through the automated workflow in
[`docs/CONTRIBUTOR-REWARDS.md`](docs/CONTRIBUTOR-REWARDS.md). First merged pull
requests are handled automatically, and maintainers can apply the `reward user`
label to manually start the same Discord and merch invite flow.
## Project Conventions
- Use Redux and existing app state patterns instead of adding new ad hoc browser storage.
@@ -20,8 +20,7 @@
// TODO(picker-schema): today this subcomponent is wired in based on
// the skill id being literally `dev-workflow`. The cleaner long-term
// path is to extend `skill.toml`'s `[[inputs]]` with an optional
// `picker = "github-issue"` discriminator and route here from that;
// see docs/skills-runner-unification.md open question 1.
// `picker = "github-issue"` discriminator and route here from that.
import createDebug from 'debug';
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -49,7 +49,6 @@ import SmartIssuePicker from './SmartIssuePicker';
//
// TODO(picker-schema): replace this hard-coded set with a schema-level
// signal in `skill.toml` — e.g. `[[inputs]] picker = "github-issue"`.
// See docs/skills-runner-unification.md open question 1.
const SMART_PICKER_SKILL_IDS = new Set(['dev-workflow']);
const SMART_PICKER_INPUT_NAMES = new Set(['repo', 'upstream', 'target_branch', 'fork_owner']);
@@ -1,8 +1,8 @@
/**
* WorkflowRunnerBody — vitest coverage for the saved-schedules block.
*
* Phase 2 of the WorkflowRunnerBody / DevWorkflowPanel unification (see
* docs/skills-runner-unification.md): this file is seeded with the
* Phase 2 of the WorkflowRunnerBody / DevWorkflowPanel unification:
* this file is seeded with the
* smoke-test for the enable/disable toggle so future Phase 3 chunks
* (run-history, active-config card, smart-issue picker gating) drop
* additional cases alongside.
@@ -20,7 +20,7 @@
* standalone route. The "card" style matches the other sections inside
* VoicePanel.
*
* Plan: docs/superpowers/plans/2026-06-02-global-ptt.md (Task 13).
* Spec: docs/superpowers/specs/2026-06-02-global-ptt-design.md.
*/
import { useCallback, useState } from 'react';
-3
View File
@@ -2,8 +2,6 @@
/**
* E2E: global push-to-talk (PTT) end-to-end flow with mocked STT.
*
* Task 14 from `docs/superpowers/plans/2026-06-02-global-ptt.md`.
*
* What this spec exercises (top to bottom):
*
* UI:
@@ -49,7 +47,6 @@
* wire). We spy on `__TAURI_INTERNALS__.invoke` before the press to
* capture the call payload.
*
* Plan: docs/superpowers/plans/2026-06-02-global-ptt.md (Task 14).
* Spec: docs/superpowers/specs/2026-06-02-global-ptt-design.md.
*
* Limitations / notes for follow-up sessions:
-368
View File
@@ -1,368 +0,0 @@
# Agent self-learning
OpenHuman learns user preferences continuously and surfaces them as ambient defaults in every system prompt. The mechanism is a small **personalization cache** materialized from multiple deterministic + LLM-driven producers, scored by stability, rendered into a user-editable `PROFILE.md`, and injected into prompts through the existing prompt-section pipeline.
This document covers how preferences are captured, scored, persisted, and surfaced. For the originating issue, see [#566](https://github.com/tinyhumansai/openhuman/issues/566).
---
## What gets learned
Six classes, encoded in the `key` prefix and stored in the existing `user_profile_facets` table:
| Class | Example facets |
|---|---|
| `style/*` | `verbosity=terse`, `format=bullets`, `preamble=skip`, `language=english`, `emoji=skip` |
| `identity/*` | `name=Sanil`, `timezone=PST`, `role=engineer`, `employer=vezures` |
| `tooling/*` | `package_manager=pnpm`, `lang=rust`, `framework=astro`, `runtime=bun` |
| `veto/*` | `tool=jest → banned`, `format=nested-bullets → banned` |
| `goal/*` | free-form goal sentences (slugified key) |
| `channel/preference` | `primary=desktop-chat` |
Recurring topics, recurring entities, and prior threads are **not** in the cache. They live in the memory tree and are retrieved per-turn by `memory_recall` under the prompt-bias instruction described below.
---
## Architecture
Four stages: **capture → identify → score → materialize**.
```
inputs substrate candidates cache
───── ───────── ────────── ─────
chat turns ──→ episodic_log ──→ Buffer (push) ──┐
+ tree │
skill syncs ──→ tree (sources) ──→ Buffer (push) ──┤
├─→ user_profile_facets
channel inbound ──→ tree ──→ Buffer (push) ──┤ (state, stability,
│ user_state, evidence)
documents ──→ tree ──→ Buffer (push) ──┘ │
stability_detector::rebuild CacheRebuilt event
(every 30 min + event-driven) │
ProfileMdRenderer
PROFILE.md
(managed blocks)
UserFilesSection
+ UserProfileSection
+ MemoryAccessSection
agent system prompt
```
Chat turns flow into the tree as `source_id="conversations:agent"` (with `tool_calls_json` stripped at canonicalize), so the same `tree_source::summariser` that already runs over Slack/Gmail/Notion now also produces summaries — and structured facet candidates — over chat content.
---
## Producers
Five producers write `LearningCandidate` values into `learning::candidate::global()`:
| # | Producer | Emits | LLM? |
|---|---|---|---|
| 1 | `composio::providers::profile::persist_provider_profile` (existing) | Identity from Gmail/Slack/Notion account fields | No |
| 2 | `learning::extract::signature::EmailSignatureSubscriber` | Identity from email signatures (last 8 lines) | No |
| 3 | `learning::extract::heuristics` (`LengthRatioDetector`, `EditWindowDetector`, `CorrectionRepeatDetector`) | Style + Veto from per-turn rolling state | No |
| 4 | `learning::ReflectionHook` (rerouted) | Goal + Style from heuristic cues and LLM-extracted reflections | Optional |
| 5 | `tree_source::summariser::llm` (extended schema) | All classes — long-tail extraction over rolling per-source summaries | Yes (existing call, extended output) |
Producer 5 is the long-tail backbone. Its prompt now asks the model to emit `{ summary, facets[] }`, and `learning::extract::summary_facets` validates each `ParsedFacet` (canonical class, mandatory `evidence_chunks`) before pushing to the buffer. **No new LLM calls** are introduced — the change extends an existing summarization round-trip with ~150400 output tokens.
Explicitly **not** producing:
- Hand-curated regex catalogs for style/identity/tooling — Producer 5 covers them and generalizes to vocabulary the catalog wouldn't predict.
- Hand-curated manifest parsers for tooling — same reasoning; Astro, Bun, Deno, uv, mise, etc. all surface through the LLM summarizer.
- Free-text NER on chat — recurring entities live in the tree's graph (from structured provider metadata) and surface contextually via `memory_recall`.
- `ToolTrackerHook` candidate emission — failure-rate is not a clean preference signal; it stays as substrate for debugging.
---
## Identification → candidate buffer
Every emission carries provenance:
```rust
pub struct LearningCandidate {
pub class: FacetClass, // Style | Identity | Tooling | Veto | Goal | Channel
pub key: String, // canonical slug, e.g. "verbosity"
pub value: String, // canonical value, e.g. "terse"
pub cue_family: CueFamily, // Explicit | Structural | Behavioral | Recurrence
pub evidence: EvidenceRef, // pointer back into substrate
pub initial_confidence: f64, // 0..=1, source-provided hint
pub observed_at: f64, // epoch seconds
}
```
`EvidenceRef` variants cover every substrate origin (`Episodic`, `EpisodicWindow`, `SourceSummary`, `TreeTopic`, `DocumentChunk`, `EmailMessage`, `Provider`, `ToolCall`, `TreeSourceWeight`). The buffer is a thread-safe bounded ring (default capacity 1024). The stability detector drains it every rebuild.
---
## Stability scoring
```
stability(class, key, value) = base × cue × user_state
base = Σ over evidence:
cue_family_weight × exp(-Δt / half_life_for_class) × log(1 + evidence_count_for_family)
cue = 2.0 if any evidence is Explicit, else 1.0
user = ∞ if Pinned, 0 if Forgotten, 1 otherwise
```
**Cue-family weights**:
| Family | Weight | Rationale |
|---|---|---|
| `Explicit` | 1.0 | direct user statement — declaration is intent |
| `Structural` | 0.9 | provider data / manifest content / signature — data doesn't lie |
| `Behavioral` | 0.7 | heuristics + summary mining — must accumulate |
| `Recurrence` | 0.6 | tree statistics — emerging |
**Class half-lives** (the time over which evidence weight halves):
| Class | Half-life |
|---|---|
| `identity` | 90 days |
| `veto` | 60 days |
| `tooling` | 30 days |
| `goal` | 30 days |
| `style` | 14 days |
| `channel` | 7 days |
**Conflict resolution per `(class, key)`**: active value = `argmax(stability)` over candidate values. Losing values are dropped from the cache; if they re-emerge they reinforce naturally through the same path.
---
## States and qualification
Two state columns govern visibility:
`state` (driven by stability):
| State | Range | Effect |
|---|---|---|
| `Active` | `stability ≥ τ_promote` (1.5) | Renders in `PROFILE.md`, injected into prompt |
| `Provisional` | `0.7 ≤ stability < 1.5` | Stored, not rendered |
| `Candidate` | `0.4 ≤ stability < 0.7` | Stays in buffer, not yet in cache |
| `Dropped` | `stability < 0.4` | Removed |
`user_state` (overrides scoring):
| State | Effect |
|---|---|
| `Auto` | Default; `state` follows scoring |
| `Pinned` | Locks `Active`; resists decay |
| `Forgotten` | Locks `Dropped`; blocks re-promotion |
**Class budgets** (top-N selection per class with a shared overflow pool):
```
style 4 · identity 4 · tooling 5 · veto 3 · goal 3 · channel 1 · overflow 5
total cap ≈ 25
```
Per-class budgets prevent a tooling-heavy user from drowning out style and identity; the overflow pool lets one class take spare capacity from another when underused.
---
## Storage
Single SQLite table — `user_profile_facets` (existing, extended by an idempotent migration in `memory::store::unified::profile::migrate_profile_schema`):
```sql
CREATE TABLE user_profile (
facet_id TEXT PRIMARY KEY,
facet_type TEXT NOT NULL, -- legacy enum
key TEXT NOT NULL,
value TEXT NOT NULL,
confidence REAL NOT NULL DEFAULT 0.5,
evidence_count INTEGER NOT NULL DEFAULT 1,
source_segment_ids TEXT,
first_seen_at REAL NOT NULL,
last_seen_at REAL NOT NULL,
-- learning-cache columns (added by migrate_profile_schema):
state TEXT NOT NULL DEFAULT 'active',
stability REAL NOT NULL DEFAULT 0.0,
user_state TEXT NOT NULL DEFAULT 'auto',
evidence_refs_json TEXT,
class TEXT,
cue_families_json TEXT,
UNIQUE(facet_type, key)
);
CREATE INDEX idx_profile_state ON user_profile(state);
CREATE INDEX idx_profile_class ON user_profile(class);
```
The provider-profile path (`composio::providers::profile::persist_provider_profile`) continues to write its existing rows untouched; class is auto-derived for backward-compatibility (provider keys like `skill:gmail:default:email` map to `class=tooling`/`identity`).
---
## Surface — `PROFILE.md`
`{workspace_dir}/PROFILE.md` carries multiple managed blocks. Each block is owned by automation; content outside the markers is user-authored and preserved across rebuilds.
```markdown
# User Profile
<!-- openhuman:style:start -->
## Style
- **verbosity**: terse
- **preamble**: skip *(pinned)*
<!-- openhuman:style:end -->
<!-- openhuman:identity:start -->
## Identity
- **name**: Sanil
- **timezone**: PST
<!-- openhuman:identity:end -->
<!-- openhuman:tooling:start -->
## Tooling
- **lang**: rust
- **package_manager**: pnpm
<!-- openhuman:tooling:end -->
<!-- openhuman:vetoes:start -->
## Vetoes
- **tool=jest**: banned
<!-- openhuman:vetoes:end -->
<!-- openhuman:goals:start -->
## Goals
- ship #566 before #686
<!-- openhuman:goals:end -->
<!-- openhuman:connected-accounts:start -->
## Connected Accounts
- gmail (sanil@vezures.xyz)
<!-- openhuman:connected-accounts:end -->
```
User-authored content between blocks (free-form notes, hand-edited details) is preserved verbatim across rebuilds.
`ProfileMdRenderer` subscribes to `DomainEvent::CacheRebuilt` and rewrites the cache-derived blocks (`style`, `identity`, `tooling`, `vetoes`, `goals`). The `connected-accounts` block remains owned by the provider sync path.
---
## Surface — prompt sections
Three sections in `agent/prompts/` cooperate:
- **`UserFilesSection`** — already-existing; injects `PROFILE.md` verbatim into the system prompt every turn.
- **`UserProfileSection`** — repointed in Phase 4 to read `FacetCache::list_active()` (gated by `LearningConfig::use_cache_for_user_profile_section`, default true). Renders `- **{class}/{key}**: {value}` bullets.
- **`MemoryAccessSection`** — new in Phase 4; static instruction biasing the agent to call `memory_recall` for entities, threads, prior decisions, recurring topics. Adds ~80 tokens to the system prompt.
The `MemoryAccessSection` covers the **contextual** half of personalization that the cache structurally cannot address: things that need to be retrieved when relevant (recurring people, prior threads) rather than always rendered.
---
## Surface — RPC controllers
Wired through the controller registry (`learning::schemas::all_learning_controller_schemas`):
| RPC | Purpose |
|---|---|
| `learning.list_facets { class }` | Enumerate the cache, optionally filtered by class. Returns active + provisional entries with provenance. |
| `learning.get_facet { class, key }` | Single-entry lookup. |
| `learning.update_facet { class, key, value }` | Overwrite the active value; auto-pins to prevent rebuild from clobbering. |
| `learning.pin_facet { class, key }` | `user_state = Pinned`. Locks `Active` regardless of stability score. |
| `learning.unpin_facet { class, key }` | `user_state = Auto`. |
| `learning.forget_facet { class, key }` | `user_state = Forgotten`. Locks `Dropped`, blocks re-promotion. |
| `learning.reset_cache {}` | Clears all `Auto` rows; preserves `Pinned`. Next rebuild repopulates from substrate. |
| `learning.rebuild_cache {}` | Manual trigger for the stability rebuild. |
| `learning.cache_stats {}` | `{ total, active, provisional, candidate, dropped, by_class }`. |
The agent itself acts as a conversational user-control surface: asked "what do you know about me?" it can call `list_facets` and cite `evidence_refs_json` for each entry; "forget that I prefer terse" calls `forget_facet("style", "verbosity")`.
---
## Configuration
`LearningConfig` in `src/openhuman/config/schema/learning.rs`:
```rust
pub struct LearningConfig {
pub enabled: bool, // master switch
pub reflection_enabled: bool,
pub user_profile_enabled: bool, // legacy hook
pub tool_tracking_enabled: bool, // substrate-only
pub reflection_source: ReflectionSource, // Local | Cloud
pub max_reflections_per_session: usize,
pub min_turn_complexity: usize,
pub chat_to_tree_enabled: bool, // pipe agent chat into tree
pub stability_detector_enabled: bool,
pub rebuild_interval_secs: u64, // default 1800 (30 min)
pub use_cache_for_user_profile_section: bool, // route prompt-section reads through cache
}
```
The summarizer-side facet emission is gated by `LlmSummariserConfig::structured_facet_extraction` (default `true`) so production deployments can disable structured extraction independently of the rest of the learning subsystem.
---
## Observability
`DomainEvent::CacheRebuilt { added, evicted, kept, total_size, rebuilt_at }` is published after every successful rebuild. Subscribers can wire personalization metrics — for example, facet-in-prompt × positive-acceptance rate — to satisfy #566's "measurable personalization improvements" criterion.
`learning.cache_stats` returns the breakdown by state and class for ad-hoc inspection.
Tracing prefixes used by new flows (filter your log stream with these):
- `[learning::candidate]`
- `[learning::extract::signature]`
- `[learning::extract::heuristics]`
- `[learning::extract::summary_facets]`
- `[learning::stability_detector]`
- `[learning::cache]`
- `[learning::profile_md]`
- `[archivist]` (chat-into-tree path)
- `[memory::tree::ingest]` (`DocumentCanonicalized` emission)
---
## Testing
Unit tests live next to their modules (`*_tests.rs` siblings; consistent with the rest of the codebase). End-to-end coverage in `tests/learning_phase4_integration_test.rs`:
- Push candidates → `stability_detector.rebuild()` → expected `CacheRebuilt` event published
- `ProfileMdRenderer` writes the expected managed blocks into `PROFILE.md`
- `learning.pin_facet` keeps an entry `Active` despite weak evidence
- `learning.forget_facet` removes an entry from `PROFILE.md` and blocks re-promotion
- `learning.list_facets` returns the expected shape and class filter
Run targeted suites:
```bash
cargo test --manifest-path Cargo.toml --lib learning::
cargo test --manifest-path Cargo.toml --lib memory::store::unified::profile
cargo test --manifest-path Cargo.toml --test learning_phase4_integration_test
```
---
## Future work
- **Validation loop** — track facet-in-prompt × positive-acceptance to calibrate cue-family weights from outcome signal rather than fixed defaults.
- **Confirmation prompts** — for high-impact, low-confidence facets (Identity, Veto), surface to the user via the agent or subconscious before promoting to `Active`.
- **Cross-corpus topic stitching** — once chat-as-tree-source has accumulated, surface unified hot topics back into the prompt as contextual hints alongside the cache.
- **Per-channel persona facets** — tooling and style differ by channel; split when signals diverge meaningfully (e.g., terser in Slack DMs, more structured in email).
- **Tool-suggestion telemetry** — distinct from the existing failure stats; needs new `ToolSuggestionAccepted`/`ToolSuggestionRejected` events before it can produce clean veto signal.
- **Soft user confirmation in PROFILE.md** — render `Provisional` rows under a quieter sub-heading so the user can promote them by editing without waiting for accumulation.
-82
View File
@@ -1,82 +0,0 @@
# Contributor Rewards Automation
OpenHuman uses `.github/workflows/contributor-rewards.yml` to invite eligible
contributors into the Discord and merch reward flow.
## Triggers
The workflow runs when:
- a pull request is merged and the PR author has no earlier merged PR in this
repository;
- a maintainer applies the `reward user` label to a pull request;
- a maintainer applies the `reward user` label to an issue;
- a maintainer runs the workflow manually to bootstrap the labels.
The workflow creates these labels if they do not already exist:
- `reward user` - maintainer-triggered reward invite;
- `reward sent` - audit label added after the invite comment is posted.
## Idempotency
Each reward comment includes a hidden marker scoped to the GitHub login:
```html
<!-- openhuman-contributor-reward:user=<login> -->
```
Automatic first-merged-PR rewards are skipped when the same login already has a
reward marker elsewhere in the repository. Maintainers can intentionally start
the flow again by applying `reward user`, but the workflow still skips a target
issue or PR that already contains the same marker.
Bot accounts are skipped.
## Configuration
Configure these repository variables under
**Settings -> Secrets and variables -> Actions -> Variables**:
| Variable | Required | Purpose |
| -------------------------------- | -------- | ------------------------------------------------------------------------ |
| `CONTRIBUTOR_REWARD_DISCORD_URL` | No | Public Discord invite URL. Defaults to `https://discord.tinyhumans.ai/`. |
| `CONTRIBUTOR_REWARD_MERCH_URL` | No | Public merch claim or redemption URL included in the comment. |
| `CONTRIBUTOR_REWARD_MESSAGE` | No | Full custom comment body. Supports tokens listed below. |
`CONTRIBUTOR_REWARD_MESSAGE` can use these tokens:
- `{user}` - GitHub mention, for example `@octocat`;
- `{login}` - raw GitHub login;
- `{discord_url}` - configured Discord URL;
- `{merch_url}` - configured merch URL or an empty string;
- `{reason}` - trigger reason;
- `{target_url}` - issue or PR URL.
Do not put private Discord invite mechanics, shipping forms, access tokens, or
other secrets in repository variables used by this workflow. Anything rendered
by the workflow is posted as a public GitHub comment.
## Security Model
The workflow uses `pull_request_target` so it can comment on pull requests from
forks. It must not check out or execute pull request code. The current workflow
only reads the GitHub event payload and calls GitHub APIs through
`actions/github-script`.
Required permissions are limited to:
- `contents: read`;
- `issues: write`;
- `pull-requests: read`.
## Manual Operation
To reward a contributor manually:
1. Open the issue or pull request.
2. Apply the `reward user` label.
3. Wait for the workflow to post the reward comment and add `reward sent`.
If the labels do not exist yet, run **Actions -> Contributor Rewards -> Run
workflow** once on `main`.
-183
View File
@@ -1,183 +0,0 @@
# Native OS Notification — Testing Status
Companion to `TAURI_CEF_FINDINGS_AND_CHANGES.md`.
This file is a quick-reference checklist: what is done, what is still needed, and how to test.
---
## What Is Done
### tauri-cef (vendored submodule)
| Change | File |
|--------|------|
| `NotifyRenderProcessHandler` wired into `TauriApp::render_process_handler` | `vendor/tauri-cef/crates/tauri-runtime-cef/src/cef_impl.rs` |
| `run_cef_helper_process()` uses `NotifyApp` (not `None`) | `vendor/tauri-cef/crates/tauri-runtime-cef/src/lib.rs` |
| `notification::unregister(browser_id)` called on browser close | `vendor/tauri-cef/crates/tauri-runtime-cef/src/cef_impl.rs` |
| Dispatch logs added (`[cef-notify] dispatch` / dropped) | `vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs` |
| `on_context_created` install logs in runtime shim | `vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs` |
| `on_context_created` install logs in helper shim | `vendor/tauri-cef/cef-helper/src/notification.rs` |
| Debug markers: `window.__OPENHUMAN_CEF_NOTIFICATION_SHIM`, `__OPENHUMAN_CEF_NOTIFICATION_ORIGIN` | `vendor/tauri-cef/cef-helper/src/notification.rs` |
| Manual test entry point: `window.__openhumanFireNotification(title, opts)` | `vendor/tauri-cef/cef-helper/src/notification.rs` |
| `ensure-tauri-cli.sh` reinstalls vendored CLI when tauri-cef sources are newer | `scripts/ensure-tauri-cli.sh` |
### tauri-plugin-notification (vendored to stop init-script conflict)
| Change | File |
|--------|------|
| Plugin vendored at `vendor/tauri-plugin-notification/` | `app/src-tauri/Cargo.toml` |
| Plugin dependency switched from git rev to local path | `app/src-tauri/Cargo.toml` |
| `.js_init_script(...)` call removed from plugin `init()` | `vendor/tauri-plugin-notification/src/lib.rs` |
**Why this mattered:** Without this change, the plugin injected a JS shim that forwarded `new Notification(...)` to `http://ipc.localhost/plugin:notification|notify`. That IPC always fails with 500 in third-party webviews (Slack), overwriting the CEF shim and blocking all notification delivery.
### openhuman-cursor app shell
| Change | File |
|--------|------|
| Default notification toggle set to `true` | `src/notification_settings/mod.rs` |
| `OPENHUMAN_DISABLE_SLACK_SCANNER=1` env bypass for DevTools inspection | `src/webview_accounts/mod.rs` |
| Platform-specific OS notification with click detection added | `src/webview_accounts/mod.rs` |
| macOS: `mac-notification-sys` + `wait_for_click` + `std::thread::spawn` | `src/webview_accounts/mod.rs` |
| Linux: `notify-rust` + `wait_for_action` + `std::thread::spawn` | `src/webview_accounts/mod.rs` |
| Windows: fire-and-forget fallback via `NotificationExt` | `src/webview_accounts/mod.rs` |
| `notification:click` Tauri event emitted with `{ account_id, provider }` | `src/webview_accounts/mod.rs` |
| `[notify-click]` success logs promoted from `debug` to `info` | `src/webview_accounts/mod.rs` |
| `mac-notification-sys = "0.6"` added to macOS dependencies | `app/src-tauri/Cargo.toml` |
| `notify-rust` added to Linux dependencies | `app/src-tauri/Cargo.toml` |
| `NotificationExt` import scoped to `#[cfg(all(feature = "cef", windows))]` | `src/webview_accounts/mod.rs` |
| `tokio::task::spawn_blocking` replaced with `std::thread::spawn` (fixes tokio panic from CEF callback thread) | `src/webview_accounts/mod.rs` |
| Scanner fallback: per-channel unread baseline, delta-based notification synthesis | `src/slack_scanner/mod.rs` |
---
## What Is Still Needed
### 1. ✅ Slack scanner registry re-enabled
**File:** `app/src-tauri/src/lib.rs`
This fix has already been applied in this PR. `ScannerRegistry` is now registered in the Tauri app
state, so the scanner-driven fallback notification path is active.
The following was added to `lib.rs` inside `tauri::Builder::default()...manage(...)`:
```rust
// already applied
.manage(Arc::new(slack_scanner::ScannerRegistry::new()))
```
With this change:
- The scanner tracks per-channel unread counts
- When a channel's unread count increases, the scanner synthesizes a native OS notification
- This is the fallback path because Slack's embedded session does not call `new Notification(...)` for real incoming messages
### 2. Verify end-to-end with a real incoming Slack message
Once the scanner registry is registered:
1. Run the app: `cd app && pnpm dev:app`
2. Open the Slack webview — wait for Slack to load fully
3. Have someone send you a Slack message from another device
4. Watch the log:
```bash
tail -f /tmp/openhuman-dev-app.log | grep --line-buffered "notify-cef\|notify-click\|scanner\|unread"
```
5. Expected log sequence:
```
[scanner] unread delta channel=... prev=N new=M
[notify-cef][<account_id>] source=... tag=... title_chars=N body_chars=M
```
6. OS toast should appear
7. Click the toast → expected:
```
[notify-click][<account_id>] clicked provider=slack
```
8. Slack webview should come into focus (frontend routes `notification:click` → `setActiveAccount` → `activate_main_window`)
### 3. Verify the CEF shim installs in Slack's page context
Before relying on real messages, confirm the helper shim is active via DevTools:
1. Open `brave://inspect`
2. Find the Slack page target → click **Inspect**
3. In Console, run:
```js
window.__OPENHUMAN_CEF_NOTIFICATION_SHIM // should be true
window.__OPENHUMAN_CEF_NOTIFICATION_ORIGIN // should be the Slack URL
```
4. If both are present, the CEF helper shim installed correctly
### 4. Verify the manual helper trigger path end-to-end
With DevTools open on the Slack target:
```js
window.__openhumanFireNotification("Slack CEF test", { body: "Manual trigger" })
```
Expected log:
```
[cef-notify] dispatch browser_id=N source=Window title="Slack CEF test" origin=...
[notify-cef][<account_id>] source=Window tag= silent=false title_chars=14 body_chars=13
```
And an OS notification toast should appear. If no toast appears, the blocker is in `forward_native_notification` in `webview_accounts/mod.rs`.
### 5. Clean up debug instrumentation (post-verification)
Once notifications are working reliably, remove:
- `window.__OPENHUMAN_CEF_NOTIFICATION_SHIM` global marker
- `window.__OPENHUMAN_CEF_NOTIFICATION_ORIGIN` global marker
- `window.__openhumanFireNotification` manual trigger
- `window.__OPENHUMAN_CEF_NOTIFICATION_CONSTRUCTOR` saved reference
- `[cef-helper-notify]` `eprintln!` calls in `cef-helper/src/notification.rs` (or replace with proper `log::debug!`)
---
## How To Run The App Correctly
The app **must** be started with `dev:app`, not `tauri dev` directly:
```bash
cd app && pnpm dev:app
```
`dev:app` sets `CEF_PATH=$HOME/Library/Caches/tauri-cef` and ensures the vendored `cargo-tauri` is installed. Without it, the app panics at startup in `cef::library_loader` with `No such file or directory`.
Live log location:
```bash
tail -f /tmp/openhuman-dev-app.log | grep --line-buffered "notify-cef\|notify-click\|scanner\|unread\|cef-notify"
```
---
## Key Log Prefixes
| Prefix | Where | Meaning |
|--------|-------|---------|
| `[cef-helper-notify] on_context_created` | renderer subprocess | shim callback fired for a new JS context |
| `[cef-helper-notify] installed shims` | renderer subprocess | shim installed in that context |
| `[cef-helper-notify] execute` | renderer subprocess | `new Notification(...)` called by page JS |
| `[cef-notify] dispatch` | browser process | notification IPC received from renderer, handler called |
| `[cef-notify] dropped` | browser process | notification IPC received but no handler registered |
| `[notify-cef][id]` | `webview_accounts` | notification payload reached app, OS toast being sent |
| `[notify-click][id] clicked` | `webview_accounts` | user clicked OS toast, emitting `notification:click` |
| `[webview-accounts] slack ScannerRegistry not in app state` | `webview_accounts` | scanner registry is missing — add `.manage(ScannerRegistry::new())` in `lib.rs` |
---
## Frontend Side (Already Wired, No Changes Needed)
`app/src/services/webviewAccountService.ts` already listens for `notification:click`:
```ts
listen('notification:click', ({ payload }) => {
dispatch(setActiveAccount(payload.account_id));
invoke('activate_main_window');
});
```
No frontend changes are needed. The click routing will work once the Rust side emits the event.
-65
View File
@@ -1,65 +0,0 @@
# Operations and Monitoring
This document describes the monitoring strategy, alert policies, and incident response procedures for the OpenHuman backend.
## Uptime Monitoring
OpenHuman uses external uptime monitors to ensure that critical backend services are available and performing within acceptable thresholds.
### Critical Endpoints
The following endpoints are monitored for uptime:
| Environment | Endpoint | Purpose | Health Signal |
|-------------|----------|---------|---------------|
| **Production** | `https://api.tinyhumans.ai/health` | Public API liveness | HTTP 200 = healthy; HTTP 503 = one or more components in error state (alert) |
| **Staging** | `https://staging-api.tinyhumans.ai/health` | Staging API liveness | HTTP 200 = healthy; HTTP 503 = one or more components in error state (alert) |
### Monitoring Providers
1. **Pingdom (Planned — not yet configured)**:
- Planned to hit the `/health` endpoints every 1 minute from multiple regions (US, EU, Asia).
- Alerts to be triggered after 2 consecutive failures.
- No Pingdom configuration currently exists in this repository; no alerts will be sent until it is set up.
2. **GitHub Actions (Active)**:
- Scheduled workflow (`.github/workflows/uptime-monitor.yml`) runs every 5 minutes.
- Serves as an independent signal from the deployment pipeline.
- On outage detection, automatically creates a labeled GitHub Issue (`bug`, `critical`, `ops`) titled **"CRITICAL: Backend Outage Detected"** and closes it when services recover, providing a durable incident log in the repository.
## Alerting and Escalation
### Alert Destinations
- **Slack/Discord**: Alerts are sent to the configured webhook (e.g. `#ops-alerts`) when the `ALERT_WEBHOOK_URL` GitHub secret is set. Set this secret in the repository settings pointing to your Slack incoming webhook or Discord server webhook URL. Alerts are skipped silently if the secret is not configured.
- **Email** *(planned)*: Email alerting to `ops@tinyhumans.ai` is not yet wired into the automated workflow. Until an email integration is added, the `ALERT_WEBHOOK_URL` webhook is the only active notification channel.
### Escalation Path
1. **Level 1 (Immediate)**: Notification to `#ops-alerts`. On-call engineer acknowledges.
2. **Level 2 (15 minutes)**: Page to the lead backend engineer.
3. **Level 3 (30 minutes)**: Escalation to the CTO.
## Incident Response (Runbook)
When a monitor fires:
1. **Verify the outage**: Check the endpoint manually or via `curl -I <endpoint>`.
2. **Check Cloud Status**: Check [DigitalOcean Status](https://status.digitalocean.com/) or other upstream providers.
3. **Review Logs**: Access runtime logs via the DigitalOcean console or your container runtime (e.g. `docker logs <container>` or Kubernetes pod logs for containers sourced from `ghcr.io`).
4. **Determine Scope**: Is it a total outage or degraded performance? Is it specific to a region?
5. **Mitigation**: Restart the service via the cloud console or redeploy the last known healthy tag.
6. **Communication**: Update the internal status and notify stakeholders if the outage exceeds 5 minutes.
## Testing Alerts
To test the GitHub Actions alert pipeline safely without causing a real outage:
1. In `.github/workflows/uptime-monitor.yml`, temporarily change one endpoint URL to a non-existent path (e.g., `/health-test-trigger`) and trigger the workflow manually via `workflow_dispatch`.
2. Verify that a GitHub Issue is created and an alert is received in the `#ops-alerts` channel.
3. Revert the URL change and trigger the workflow again; verify the issue is closed and a recovery notification is sent.
To test the Pingdom monitor, use Pingdom's built-in test-alert feature from the dashboard rather than changing the monitored URL.
## Maintenance
During planned maintenance, monitors should be paused to avoid false positives. This is handled via the provider's "Maintenance Mode" or by disabling the GitHub Action temporarily.
-86
View File
@@ -1,86 +0,0 @@
# Portfolio Readiness
This note is for engineering review and portfolio handoff. It does not change
the product surface or marketing README.
## What This Repo Demonstrates
- A large local-first desktop AI product with TypeScript UI, Rust/Tauri shell,
and a Python/Rust-adjacent core surface.
- Integration-heavy product architecture: local memory, account connectors,
desktop shell commands, native tools, and testable app services.
- Real validation breadth: TypeScript compile, ESLint, Vitest, and Rust
mock-backed tests all run locally.
## Current Validation Evidence
Run from the repo root:
```bash
pnpm run typecheck
pnpm run lint
pnpm --filter openhuman-app exec vitest run --config test/vitest.config.ts src/pages/__tests__/Conversations.test.tsx src/pages/__tests__/Conversations.render.test.tsx src/components/settings/panels/__tests__/RecoveryPhrasePanel.test.tsx
pnpm --filter openhuman-app exec prettier --check .
cargo fmt --manifest-path Cargo.toml --all --check
cargo fmt --manifest-path app/src-tauri/Cargo.toml --all --check
cargo check --manifest-path app/src-tauri/Cargo.toml
git diff --check
```
Latest clean-branch portfolio-readiness run:
- `pnpm run typecheck`: passed.
- `pnpm run lint`: passed with 35 warnings. The remaining warning family is
React compiler `set-state-in-effect`.
- Focused Vitest coverage for the touched React areas passed: `3` files and
- `24` tests across Conversations and Recovery Phrase panel tests.
- `pnpm --filter openhuman-app exec prettier --check .`: passed from `app/`.
- Rust format checks for the root and Tauri manifests passed.
- `cargo check --manifest-path app/src-tauri/Cargo.toml`: passed with existing
Rust warnings.
- `git diff --check`: passed.
## Cleanup Performed
- Moved mnemonic/recovery-phrase mode resets into the explicit mode switch
handlers.
- Removed dead sidebar label reset state now that conversation labels use a
fixed tab model.
- Ignored local `.cocoindex_code/` index output so code-index experiments do
not dirty the repo.
- Recorded validation evidence in `CODEX_WORKPAD.md`.
## Remaining Presentation Debt
- The lint output is not presentation-clean yet because 35 React compiler
warnings remain.
- Most warnings are synchronous state updates inside effects. Some may be
harmless legacy patterns, but they should be either refactored or explicitly
accepted as a policy before this repo is used as a polished flagship example.
- Vitest currently emits repeated Node `localStorage is not available` warnings;
tests pass, but the environment warning should be silenced or documented.
## Public Claim Boundary
Safe to claim:
- The repository has broad local validation across TypeScript, lint, JS tests,
and Rust tests.
- The current cleanup reduced generic lint noise without changing product
behavior.
Do not claim yet:
- "Lint-clean" or "warning-free."
- Full UI runtime readiness across every Tauri/desktop flow.
- That the remaining React compiler warnings have been reviewed and accepted.
## Next Slice
Create a narrow lint-policy slice:
1. Pick one warning family, starting with `react-hooks/set-state-in-effect`.
2. Classify warnings into real refactors vs accepted legacy patterns.
3. Fix the highest-risk components first.
4. Keep `pnpm run typecheck`, `pnpm run lint`, and the relevant Vitest tests
green after each group.
-96
View File
@@ -1,96 +0,0 @@
# Prompt Injection Guard
This document describes the end-to-end prompt injection detection and enforcement flow added in the OpenHuman core and app.
## Scope
- Backend enforcement is authoritative.
- Frontend checks are advisory UX only.
- Guarding runs before model inference or agent/tool loop execution for user-supplied prompts.
## Detection Layers
Detection is implemented in `src/openhuman/prompt_injection/` with layered analysis:
1. Normalization
- Lowercasing and whitespace collapse.
- Obfuscation cleanup (zero-width chars, punctuation noise, basic leetspeak substitutions).
- Compact-string pass for spaced-out attacks (`i g n o r e ...`).
2. Pattern rules
- Instruction override patterns (`ignore/disregard/forget previous instructions`).
- Role hijack patterns (`you are now`, `developer mode`, `jailbreak`).
- Prompt/system exfiltration patterns (`reveal system prompt`, `show developer instructions`).
- Secret exfiltration patterns (`api key`, `token`, `password`, etc.).
- Unsafe tool coercion patterns.
3. Optional classifier
- Enabled with `OPENHUMAN_PROMPT_INJECTION_CLASSIFIER=heuristic`.
- Adds score for suspicious combinations (obfuscation + override/exfiltration intent).
## Verdict Contract
Detector returns:
- `verdict`: `allow | block | review`
- `score`: normalized `0.0..1.0`
- `reasons`: stable reason codes/messages
- `action`: enforcement action (`allow`, `block`, `review_blocked`)
Current threshold policy:
- `score >= 0.70` -> `block`
- `0.45 <= score < 0.70` -> `review`
- `< 0.45` -> `allow`
## Enforcement Flow
Server-side checks are wired before LLM/tool execution in:
- `src/openhuman/channels/providers/web.rs` (`start_chat`)
- `src/openhuman/local_ai/ops.rs` (`agent_chat`, `agent_chat_simple`, `local_ai_chat`, `local_ai_prompt`, `local_ai_vision_prompt`, `local_ai_summarize`)
- `src/openhuman/agent/harness/session/runtime.rs` (`Agent::run_single`)
- `src/openhuman/agent/bus.rs` (`agent.run_turn` native bus handler)
If action is `block` or `review_blocked`, request processing is stopped and no prompt is sent to provider/tool loop.
## Frontend Advisory UX
- Advisory pre-submit validation in `app/src/chat/promptInjectionGuard.ts`.
- Composer integration in `app/src/pages/Conversations.tsx`.
- `block` verdict: advisory warning is shown client-side; backend remains authoritative for final enforcement.
- `review` verdict: advisory warning shown; backend still enforces final decision.
## Logging and Privacy
Each backend decision logs:
- `request_id`
- `user_id`
- `session_id`
- `source`
- `verdict`
- `score`
- `reasons` (codes)
- `action`
- `prompt_hash` (SHA-256)
- `prompt_chars`
Raw prompt text is not logged by this guard.
## Tests
- Unit tests:
- `src/openhuman/prompt_injection/tests.rs`
- `src/openhuman/channels/providers/web_tests.rs`
- `src/openhuman/local_ai/ops_tests.rs`
- `app/src/chat/__tests__/promptInjectionGuard.test.ts`
- Integration test:
- `tests/json_rpc_e2e.rs` (`json_rpc_prompt_injection_is_rejected_before_model_call`)
## Extending Rules
1. Add/adjust regex rules in `src/openhuman/prompt_injection/detector.rs` (`DETECTION_RULES`).
2. Keep reason codes stable for observability and tests.
3. Add unit tests for both positive and negative cases (including obfuscated variants).
4. If introducing new classifier logic, gate it behind config/env and ensure deterministic fallback behavior when disabled.
-213
View File
@@ -1,213 +0,0 @@
# OpenHuman Security Audit — Architecture & Data Flow Analysis
> Date: 2026-05-21
> Author: JAYcodr (fork analysis, not an official audit)
> Scope: Architecture overview, trust boundaries, credential flow, attack surface
---
## 1. System Overview
OpenHuman is a desktop AI assistant with a **Rust core** running in-process inside a Tauri desktop host, and a **React/TypeScript frontend**. Communication between frontend and core happens via two channels:
| Channel | Protocol | Auth |
|---|---|---|
| Primary | Socket.IO (bidirectional streaming) | Session-baked connection auth |
| Secondary | HTTP JSON-RPC | Basic Auth (`WWW-Authenticate` realm) |
**No sidecar binary** — core runs as a tokio task inside the Tauri process (`core_process.rs`).
---
## 2. Module Map
### Core (`src/openhuman/`) — 66 domains
| Category | Domains |
|---|---|
| Agent | `agent`, `agent_experience`, `agent_tool_policy` |
| Memory | `memory` (stm_recall, docs), `embeddings`, `learning`, `workspace` |
| Skills | `skills` (metadata-only), `mcp_client`, `mcp_clients`, `mcp_server`, `composio` |
| Channels | `channels` (dispatch), `telegram`, `discord`, `whatsapp_data`, `webview_accounts` |
| Infrastructure | `http_host`, `socket` (Socket.IO server), `runtime_node`, `runtime_python` |
| Business Logic | `billing`, `credentials`, `vault`, `encryption`, `notifications`, `webhooks`, `approval`, `cron`, `meet`, `meet_agent`, `team`, `threads`, `todos` |
| UI-adjacent | `accessibility`, `autocomplete`, `screen_intelligence`, `voice` |
| Other | `config`, `health`, `heartbeat`, `doctor`, `migration`, `update`, `security`, `prompt_injection` |
### Transport (`src/core/`)
| File | Role |
|---|---|
| `src/core/jsonrpc.rs` | JSON-RPC over HTTP, method dispatch |
| `src/core/socketio.rs` | Socket.IO server, `WebChannelEvent` struct for streaming |
| `src/core/auth.rs` | HTTP Basic Auth handler |
| `src/openhuman/http_host/rpc.rs` | JSON-RPC endpoint (`list()` function) |
| `src/openhuman/http_host/auth.rs` | `WWW-Authenticate` header, `unauthorized_response()` |
### Event Bus (`src/core/event_bus/`)
Typed pub/sub + in-process typed request/response:
```text
publish_global(DomainEvent) → fire-and-forget broadcast
register_native_global(method, handler) → one-to-one typed dispatch
request_native_global(method, req) → call and wait for response
```
**Domain events:** `agent`, `memory`, `channel`, `skill`, `tool`, `webhook`, `mcp_client`, `system`, `approval`, `cron`, `triage`
---
## 3. Credential & Token Flows
### Core RPC Auth
- HTTP JSON-RPC protected by **HTTP Basic Auth**
- Realm: `"OpenHuman Hosted Directory"`
- Per-launch bearer token, transported differently per deployment shape:
- **Desktop / Tauri shell**: bearer is generated in `CoreProcessHandle::new()` and held in-memory as `CoreProcessHandle.rpc_token: Arc<String>`, then handed to the embedded server via an internal in-memory handle (`run_server_embedded_with_ready(rpc_token: Some(_))`). **Not** published to the process environment.
- **Standalone CLI / Docker / cloud**: bearer is read from the `OPENHUMAN_CORE_TOKEN` env var (via `init_rpc_token`) or from the `{workspace}/core.token` file. This is the operator-supplied configuration surface for those deployments and is intentional.
- Frontend obtains bearer via `invoke('core_rpc_token')` Tauri command
### Stored Credentials
- `credentials` domain manages credential storage
- `encryption` domain handles at-rest encryption
- `auth-profiles.json` — auth data referenced by `settings.ai.apiKeysEncrypted` i18n key
### MCP Server Auth
- Composio API key stored via `settings.composio.apiKeyStoredPlaceholder`
- MCP client config (Claude Desktop, Cursor, Codex, Zed) generated in settings panel
---
## 4. Trust Boundaries & Attack Surface
### Boundary 1: External Channels (Telegram, Discord, WhatsApp, etc.)
- Inbound messages from third-party messaging platforms flow through `channels/runtime/dispatch.rs`
- Each provider scanner runs as native CDP/scraping — **no JS injection** in migrated providers
- `ChannelInboundMessage` event published to event bus
**Risk:** Third-party message content is untrusted. Prompt injection possible if message content is rendered or echoed without sanitization. The `prompt_injection` domain exists as a guard.
### Boundary 2: MCP Tool Bridge (`mcp_client/`, `mcp_clients/`)
- External MCP servers connect via stdio or HTTP
- Tools exposed through `tool_registry`
- `McpClientToolExecuted` events published
**Risk:** MCP tools are external services. Tool output flows back into agent context. No obvious output sanitization in the tool execution path.
### Boundary 3: Skill Runtime (Removed)
- QuickJS / `rquickjs` runtime was **removed** (PR #1061)
- `src/openhuman/skills/` is now metadata-only
- No dynamic code execution from skill packages
**Risk:** Significantly reduced vs. prior architecture.
### Boundary 4: Local File System Access
- `workspace`, `vault`, `webview_accounts` domains have file system access
- `screen_intelligence`, `accessibility` domains capture screen content
- Memory stored via `memory` domain
**Risk:** Screen capture and file access are high-privilege operations. Controlled by macOS permissions (Accessibility, Screen Recording).
### Boundary 5: MCP Server Config File
- Settings panel generates `~/.config/openhuman/mcp.json` for external MCP clients
- Config written via `settings.mcpServer.openConfigFile` / `writeFile`
- Path exposed via `settings.mcpServer.configFilePath`
**Risk:** If `mcp.json` is world-readable, token theft possible. Worth auditing file permissions on the config directory.
---
## 5. Data Flows
### Agent Turn (primary AI interaction)
```text
External message → channels/runtime/dispatch.rs
→ request_native_global("agent.run_turn", AgentTurnRequest)
→ agent/bus.rs: run_tool_call_loop()
→ tool_registry → SkillExecution events
→ on_delta mpsc channel → WebChannelEvent (Socket.IO)
→ frontend (SocketIOMCPTransportImpl)
```
### Memory Recall
```text
Tool call: memory.recall → memory/stm_recall/recall.rs: stm_recall()
→ MemoryRecalled event on event bus
→ consumed by skill/mcp_client subscribers
```
### Credential Setup
```text
Frontend settings → core RPC (JSON-RPC over HTTP + Basic Auth)
→ credentials domain → encryption domain
→ stored to auth-profiles.json
```
---
## 6. Security Observations (Not Exhaustive)
### Areas Worth Auditing
1. **Prompt injection from channel messages**`prompt_injection` domain exists; need to verify it's applied to all channel inbound paths and not just chat UI
2. **MCP tool output sanitization** — external MCP tool output flows into agent context without obvious filtering
3. **Config directory permissions**`~/.config/openhuman/` and `mcp.json` permission model not reviewed
4. **Credential encryption**`encryption` domain used for at-rest encryption; key management model unclear
5. **WebView CSP** — embedded webviews (Telegram, Discord, etc.) loaded under CEF — need to verify CSP headers and iframe restrictions
6. ~~**`OPENHUMAN_CORE_TOKEN` in process env** — bearer token in env var; visible via `/proc/self/environ` on Linux or process inspection on macOS~~**resolved**: in-process core now receives the bearer via an in-memory handoff (`run_server_embedded_with_ready(rpc_token: Some(_))`); the env-var crossing has been removed from the Tauri shell's spawn path. CLI / docker / cloud env-as-config remains the intended operator surface for those deployments.
7. **No rate limiting observed** on HTTP JSON-RPC endpoint
### Positive Signals
- QuickJS skill runtime removed — large attack surface eliminated
- CEF webviews for migrated providers have **zero injected JS** — good isolation
- MCP server stdio transport provides sandboxing for external tools
- `security` domain exists — may contain hardening measures not reviewed here
---
## 7. Recommended Next Steps (for Maintainers)
- [ ] Audit `prompt_injection` domain coverage — is it applied to all channel inbound paths?
- [ ] Document `encryption` domain key management
- [ ] Check file permissions on `~/.config/openhuman/`
- [ ] Add rate limiting to HTTP JSON-RPC endpoint
- [ ] Document MCP tool output handling expectations
- [x] Review `OPENHUMAN_CORE_TOKEN` lifetime and exposure scope — in-process core now uses in-memory handoff (no env crossing); CLI/docker/cloud retain env-as-config
---
## 8. RPC Method Reference
JSON-RPC methods follow `domain_operation` pattern:
```text
memory_recall_memories
memory_recall_context
thread_turn_state_lifecycle
wallet_setup_round_trips_status
tool_registry_lists_and_gets_entries
```
Native (event bus) methods:
```text
agent.run_turn → agent/bus.rs
memory.sync → memory/bus.rs
```
---
*This document is an independent analysis, not an official security assessment.*
-505
View File
@@ -1,505 +0,0 @@
# Tauri CEF Notification Findings And Changes
## Scope
This note summarizes:
- what was found in `tauri-cef`
- what was missing for webview notification permission and delivery
- what was changed in `tauri-cef`
- what was changed in `openhuman-cursor`
- how the setup was debugged and verified
Relevant codebases:
- `(external clone, not in this repo)` — standalone `tauri-cef` repo
- `.` (repo root)
- vendored submodule: `app/src-tauri/vendor/tauri-cef`
## Initial Findings In `tauri-cef`
### 1. Browser-side permission acceptance existed
`tauri-runtime-cef` already had browser-process logic that accepted Chromium notification permission requests:
- `crates/tauri-runtime-cef/src/permissions.rs`
- `crates/tauri-runtime-cef/src/cef_impl.rs`
This meant CEF could accept `CEF_PERMISSION_TYPE_NOTIFICATIONS`.
### 2. Renderer-side granted state was not wired into the real runtime path
Slack and similar apps do not rely only on the browser permission callback. They also inspect browser-visible JavaScript state:
- `Notification.permission`
- `Notification.requestPermission()`
- `navigator.permissions.query({ name: "notifications" })`
A renderer-side shim for this behavior existed only in:
- `cef-helper/src/notification.rs`
But the actual runtime app path used by `tauri-runtime-cef` did not attach that renderer process handler in the default `TauriApp` path. As a result, web apps could still observe notification state as `"prompt"` instead of `"granted"`.
### 3. Notification permission looked partially implemented, not end-to-end
There was a notification IPC path and registry in `tauri-runtime-cef`, but the setup was incomplete unless the embedder registered a handler:
- browser process received notification IPC
- runtime exposed `notification::register(...)`
- without an app-side registration, notifications could still be dropped
### 4. The old behavior was not sufficient for Slack
In practice, Slack kept behaving as if notifications still needed to be enabled because the renderer-visible granted state was not consistently exposed on the real runtime path.
### 5. Additional root cause found during live debugging
Later live DevTools inspection revealed a second, more concrete failure mode in `openhuman-cursor`:
- `tauri-plugin-notification` injects its own JavaScript init script into every Tauri webview
- that init script overwrote `window.Notification`
- the replacement implementation forwarded notification calls to:
- `plugin:notification|notify`
- over Tauri IPC at `http://ipc.localhost/...`
For external web pages such as Slack, this is the wrong path:
- the page is not supposed to use Tauri IPC as its notification transport
- the page should stay on the native CEF notification path
- when the plugin shim won, calls failed with `500` and console errors such as:
- `POST http://ipc.localhost/plugin%3Anotification%7Cnotify 500`
- `Origin header is not a valid URL`
That meant the plugins JS shim was effectively undoing the CEF notification interception fix inside external webviews.
## Changes Made In `tauri-cef`
These changes were made in the standalone `tauri-cef` repo and pushed there first.
### 1. Moved notification permission shims into the real runtime path
Renderer-side notification permission shims were added to `tauri-runtime-cef` so they run on the real Tauri CEF runtime path instead of only in the standalone helper.
Files involved:
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/notification.rs`
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/cef_impl.rs`
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/lib.rs`
Behavior provided by the shim:
- `Notification.permission` resolves to granted
- `Notification.requestPermission()` resolves to granted
- `navigator.permissions.query({ name: "notifications" })` reports granted
- notification calls are forwarded through the CEF runtime notification path
### 2. Hooked the render process handler into `TauriApp`
The runtime app path now installs the render process handler needed for the notification shim.
### 3. Started the helper process with a real app object
`run_cef_helper_process()` was updated to launch CEF with an app object instead of `None`, so the notification renderer setup is available consistently.
### 4. Added notification handler cleanup on browser close
In the vendored `tauri-cef` inside `openhuman-cursor`, an additional lifecycle cleanup was added:
- `app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/cef_impl.rs`
Added on browser close:
```rust
crate::notification::unregister(browser_id);
```
This prevents stale notification handlers from remaining registered after a webview is destroyed.
### 5. Standalone `tauri-cef` commit
The standalone `tauri-cef` repo was updated and pushed with:
- branch: `feat/cef`
- commit: `c8ece7c78`
- message: `Fix CEF notification permission shims`
The `openhuman-cursor` vendored submodule was then updated to the corresponding submodule commit:
- `c8ece7c784b8cdff16dc552f6892a0c9982ef1ba`
## Changes Made In `openhuman-cursor`
### 1. Enabled shell-side webview notifications by default
File:
- `app/src-tauri/src/notification_settings/mod.rs`
Change:
- default notification toggle changed to enabled:
```rust
AtomicBool::new(true)
```
This ensures notifications are not disabled by default at the app layer.
### 2. Added a Slack debugging bypass for internal CDP attachment
File:
- `app/src-tauri/src/webview_accounts/mod.rs`
Environment flag added:
- `OPENHUMAN_DISABLE_SLACK_SCANNER=1`
When set for Slack accounts, the app now:
- skips Slack scanner startup
- skips the long-lived CDP session for Slack
- loads the real Slack URL directly instead of going through the placeholder `data:` path used for the CDP bootstrap flow
Expected logs:
- `[webview-accounts] skipping CDP session via OPENHUMAN_DISABLE_SLACK_SCANNER ...`
- `[webview-accounts] slack scanner disabled via OPENHUMAN_DISABLE_SLACK_SCANNER ...`
This was added only to make manual DevTools inspection possible without the app attaching to the same Slack target.
### 3. Vendored `tauri-plugin-notification` and removed its JS init script
To stop `window.Notification` from being overwritten inside external webviews, `tauri-plugin-notification` was vendored into the repo and switched from a git dependency to a path dependency.
New vendored path:
- `app/src-tauri/vendor/tauri-plugin-notification`
Two changes were made:
1. The plugin dependency in:
- `app/src-tauri/Cargo.toml`
now points to the vendored path.
2. The plugin init function in:
- `app/src-tauri/vendor/tauri-plugin-notification/src/lib.rs`
no longer calls:
```rust
.js_init_script(...)
```
This keeps the Rust-side desktop notification API available through `NotificationExt`, but stops the plugin from globally replacing `window.Notification` in embedded external pages.
The result is:
- app Rust code can still fire native notifications
- external webviews like Slack no longer get forced onto the Tauri IPC notification path
- the native CEF notification shim remains authoritative inside the external webview
## Verification Performed
### Build verification
In `app/src-tauri`:
- `cargo fmt` passed
- `cargo check --features cef` passed for the main notification changes
- `cargo check --features cef` also passed after vendoring `tauri-plugin-notification` and removing its JS init script
One later `cargo check` attempt for the Slack DevTools bypass work was blocked by another running Cargo process holding the build lock, but the changes were localized and formatting completed.
### Runtime verification
The app was run in dev mode and the CEF DevTools target list was checked through:
- `http://localhost:9222/json/list`
- `http://127.0.0.1:9222/json/list`
- earlier in one run, `http://[::1]:9222/json/list`
Observed targets included:
- Slack page target
- Slack service worker target
- OpenHuman page target
This confirmed:
- Slack was running inside the CEF webview
- CEF remote debugging was active
### Slack scanner contention diagnosis
At first, the main reason DevTools inspection failed for Slack was that the app itself was attaching to the Slack target over CDP:
- the Slack scanner auto-attached
- the long-lived per-account CDP session also attached
This was why manual DevTools sessions disconnected even when the target existed.
The debugging bypass confirmed this by producing logs that both internal attachment paths were skipped.
## Final DevTools Findings
After the Slack-specific CDP paths were disabled, DevTools could still disconnect even for the `OpenHuman` page target. That showed the remaining issue was not Slack-specific.
Live verification showed:
- the active backend was on `127.0.0.1:9222`
- `localhost` was inconsistent during checks
- Brave already had multiple established connections to `127.0.0.1:9222`
- the running `OpenHuman` app was listening on that port
Most likely explanation:
- multiple existing DevTools frontend sessions in Brave were already connected
- reopening new inspector tabs caused connection churn
- `127.0.0.1` was more reliable than `localhost`
Recommended DevTools usage:
1. Close all existing DevTools tabs for `localhost:9222` and `127.0.0.1:9222`.
2. Reopen only one inspector at a time.
3. Prefer the exact `127.0.0.1` websocket host advertised by `/json/list`.
## Later Runtime And Helper Findings
### 1. The real helper bundle was stale at first
During live verification, the main app binary contained the new notification instrumentation but the bundled macOS helper apps did not.
That turned out to be a packaging issue:
- the installed `cargo-tauri` binary was stale
- helper executables are bundled by the CLI/bundler
- rebuilding the app alone was not enough to refresh the helper binaries
To prevent this from recurring, the local CLI bootstrap script was updated:
- `scripts/ensure-tauri-cli.sh`
It now reinstalls vendored `cargo-tauri` when vendored `tauri-cef` sources are newer than the installed CLI binary.
### 2. The live macOS renderer path uses `cef-helper`
An important debugging detail was that the actual live renderer helper on macOS was using:
- `app/src-tauri/vendor/tauri-cef/cef-helper/src/notification.rs`
and not only the runtime-side notification file in:
- `app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs`
So the helper-side shim was instrumented directly with:
- install logs
- execution logs
- debug markers
- manual test entry points
Helper-side logs added:
- `[cef-helper-notify] on_context_created ...`
- `[cef-helper-notify] installed shims ...`
- `[cef-helper-notify] execute source=...`
Helper-side debug markers added:
- `window.__OPENHUMAN_CEF_NOTIFICATION_SHIM`
- `window.__OPENHUMAN_CEF_NOTIFICATION_ORIGIN`
- `Notification.__openhuman_cef`
Manual helper entry points added:
- `window.__openhumanFireNotification(title, options)`
- `window.__OPENHUMAN_CEF_NOTIFICATION_CONSTRUCTOR`
### 3. Helper shim installation was verified in the real Slack page
After rebuilding the helper bundle, live DevTools checks showed:
- `window.__OPENHUMAN_CEF_NOTIFICATION_SHIM === true`
- `window.__OPENHUMAN_CEF_NOTIFICATION_ORIGIN` pointing at the Slack page URL
This proved that the CEF helper render shim was actually installing in the Slack page context.
### 4. Manual helper-triggered notifications work end-to-end
The following manual trigger was verified to reach the app:
```js
window.__openhumanFireNotification("Slack CEF test", {
body: "Manual helper path"
})
```
Observed runtime logs:
- `[cef-notify] ipc ...`
- `[cef-notify] dispatch ...`
- `[notify-cef][...] ...`
This proved that:
- renderer helper -> browser IPC works
- runtime dispatch works
- `openhuman-cursor` receives the notification payload
- OS notification delivery can work
### 5. Tokio runtime panic found and fixed in app notification delivery
Once the manual helper path reached the app, native notification delivery initially crashed with:
- `there is no reactor running, must be called from the context of a Tokio 1.x runtime`
The panic came from using `tokio::task::spawn_blocking(...)` from a CEF callback thread in:
- `app/src-tauri/src/webview_accounts/mod.rs`
Fix applied:
- replaced `tokio::task::spawn_blocking(...)` with `std::thread::spawn(...)`
This was done for the macOS notification path and the Linux path for consistency.
After this fix, the manual helper trigger produced an actual OS notification successfully.
### 6. `Invalid UTF-16 string` messages were observed but were not the blocker
During manual notification tests, CEF emitted:
- `Invalid UTF-16 string`
These warnings appeared before successful notification IPC and dispatch logs, so they were treated as noisy string-conversion warnings rather than the root failure.
### 7. Slack still does not automatically use the browser notification APIs for real messages
After helper-side execution logging was added, a real incoming Slack message did **not** produce:
- `[cef-helper-notify] execute source=0 ...`
- `[cef-helper-notify] execute source=1 ...`
This means the real incoming-message path in this embedded Slack session is not currently hitting:
- `new Notification(...)`
- `ServiceWorkerRegistration.prototype.showNotification(...)`
So the remaining problem is no longer CEF notification transport. The remaining problem is Slack-specific runtime behavior in this embedded environment.
### 8. An attempted hard lock on `window.Notification` broke Slack rendering
One experiment tried to prevent Slack from overwriting the helper-installed notification hooks by making them effectively non-overridable.
That caused Slack to render a blank screen, so that hardening change was reverted.
Current safe state:
- Slack renders normally
- helper shim installs
- manual helper trigger works
- automatic incoming-message notifications still do not use the browser notification APIs
### 9. Fallback strategy: synthesize notifications from the Slack scanner
Because Slacks own incoming-message path was not invoking browser notification APIs, a fallback path was added to the Slack scanner:
- `app/src-tauri/src/slack_scanner/mod.rs`
- `app/src-tauri/src/webview_accounts/mod.rs`
The scanner logic was updated to:
- keep a per-channel unread baseline
- skip notifications on the first scan
- emit a native notification when a channel unread count increases later
However, live verification showed the scanner path was not actually active because the app was missing the scanner registry in managed state.
Observed log:
- `[webview-accounts] slack ScannerRegistry not in app state`
So the scanner-based fallback is patched in code, but it still needs the app builder to manage `slack_scanner::ScannerRegistry::new()` in:
- `app/src-tauri/src/lib.rs`
## Current Outcome
### Notification permission behavior
The underlying notification permission problem in `tauri-cef` was fixed by moving the renderer-visible granted-state shim into the actual runtime path.
This means Slack-style checks should now see notification permission as granted inside the Tauri CEF webview.
### App-side notification behavior
`openhuman-cursor` was updated so:
- shell-side notifications are enabled by default
- the vendored `tauri-cef` includes the runtime permission fix
- browser notification handlers are cleaned up on webview close
- manual helper-triggered notifications reach the OS successfully
- app-side native notification delivery no longer depends on a Tokio runtime in the callback thread
### Remaining distinction
There are two separate concerns:
1. Permission/granted-state correctness
2. What the app does after a notification is received
The changes above fix the permission side and the runtime notification bridge plumbing. The app still needs its normal notification handling path to decide how to present or forward those notifications.
### Current verified status
Verified working:
- notification permission appears granted in the Slack webview
- helper shim installs in the live Slack page
- manual helper notification trigger reaches CEF browser IPC
- runtime dispatch reaches `openhuman-cursor`
- native OS notification display works for the manual helper-triggered path
Still not working automatically:
- real Slack incoming messages do not currently surface through browser notification APIs in this embedded session
- scanner-driven fallback notifications will not run until the scanner registry is re-enabled in app state
## Files Changed
### In `tauri-cef`
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/notification.rs`
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/cef_impl.rs`
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/lib.rs`
- `(external tauri-cef repo)/CEF_NOTIFICATION_PERMISSION_CHANGES.md`
### In `openhuman-cursor`
- `app/src-tauri/src/notification_settings/mod.rs`
- `app/src-tauri/src/webview_accounts/mod.rs`
- `app/src-tauri/src/slack_scanner/mod.rs`
- `app/src-tauri/src/lib.rs`
- `app/src-tauri/Cargo.toml`
- `app/src-tauri/vendor/tauri-plugin-notification/Cargo.toml`
- `app/src-tauri/vendor/tauri-plugin-notification/src/lib.rs`
- `app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/cef_impl.rs`
- `app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs`
- `app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/lib.rs`
- `app/src-tauri/vendor/tauri-cef/cef-helper/src/notification.rs`
- `app/src-tauri/vendor/tauri-cef/cef-helper/src/main.rs`
- `scripts/ensure-tauri-cli.sh`
## Suggested Follow-Up
Recommended next functional fix:
1. Re-enable `slack_scanner::ScannerRegistry::new()` in:
- `app/src-tauri/src/lib.rs`
2. Rebuild and verify unread-delta notifications from the scanner fallback path.
Secondary cleanup work:
1. Remove temporary helper/debug instrumentation once notification behavior is finalized.
2. If cleaner inspection is still needed, move remote debugging off `9222` to a dedicated port such as `9333`.
-178
View File
@@ -1,178 +0,0 @@
# Codex PR Checklist
Use this checklist for Codex web sessions, Linear-launched implementation agents, and any other remote agent that opens OpenHuman PRs.
## Required Preflight
Run the scriptable preflight wrapper (recommended):
```bash
node scripts/codex-pr-preflight.mjs --strict-path --lightweight
```
Use `--lightweight` when you only need environment/repo checks plus changed-surface validation recommendations (it skips heavier runtime validations).
Run this before editing files:
```bash
pwd
git status --porcelain
git branch --show-current
git remote -v
test -f AGENTS.md
test -f gitbooks/developing/README.md
test -f Cargo.toml
test -f app/package.json
```
Expected repository path in Codex web is `/workspace/openhuman`. If the checkout is missing or the command shows another project, stop immediately and report the environment binding problem. Do not edit files in the wrong repository.
## Launch Trigger Rule
Use exactly one Codex trigger per Linear issue.
Preferred launch pattern:
```md
@Codex use the Codex environment for jwalin-shah/openhuman.
Work issue <ISSUE-KEY>.
Expected path: /workspace/openhuman.
Start from latest origin/main.
Create branch codex/<ISSUE-KEY>-<short-title>.
Follow docs/agent-workflows/codex-pr-checklist.md exactly.
Do not open duplicate PRs. If validation is blocked, report exact command and error in the PR body and Linear.
```
Do not also set `delegate: Codex` when posting an explicit `@Codex` launch comment. Linear delegate metadata can start its own Codex thread, so combining both mechanisms can double-trigger the same issue.
If using `delegate: Codex` as the only trigger for an integration that requires it, do not add an `@Codex` comment. Record in the issue which trigger was used.
## Branch And PR Rules
- Start from latest `origin/main` unless the Linear issue explicitly says otherwise.
- Use one branch and one PR per Linear issue.
- Name branches `codex/<ISSUE-KEY>-<short-title>`.
- Do not open duplicate PRs for the same issue. If a retry is needed, update the existing PR branch or close the stale duplicate and state which PR is canonical.
- PRs should target `jwalin-shah/openhuman:main` unless upstream permissions allow `tinyhumansai/openhuman:main`.
## Duplicate PR Cleanup
Canonical PR rule: keep the PR whose head branch is the active issue branch and whose head commit contains the intended final work. If two PRs contain equivalent work, keep the PR already linked from Linear or already carrying useful review/CI history. If neither has history, keep the older PR number to reduce churn. Do not choose by recency alone; compare the heads first and move any useful commits or PR body details onto the canonical PR before closing the duplicate.
Lightweight comparison and close recipe:
```bash
BASE_REPO=tinyhumansai/openhuman # or jwalin-shah/openhuman for fork-targeted PRs
BASE_REMOTE=upstream # remote matching BASE_REPO
KEEP=123 # canonical PR number
CLOSE=124 # duplicate PR number
gh pr view "$KEEP" --repo "$BASE_REPO" --json number,url,state,baseRefName,headRefName,headRefOid
gh pr view "$CLOSE" --repo "$BASE_REPO" --json number,url,state,baseRefName,headRefName,headRefOid
git fetch "$BASE_REMOTE" "refs/pull/$KEEP/head:refs/tmp/pr-$KEEP"
git fetch "$BASE_REMOTE" "refs/pull/$CLOSE/head:refs/tmp/pr-$CLOSE"
git log --oneline --left-right --cherry-pick "refs/tmp/pr-$KEEP...refs/tmp/pr-$CLOSE"
git diff --stat "refs/tmp/pr-$KEEP...refs/tmp/pr-$CLOSE"
git diff --name-status "refs/tmp/pr-$KEEP...refs/tmp/pr-$CLOSE"
gh pr close "$CLOSE" --repo "$BASE_REPO" --comment "Closing as a duplicate of #$KEEP for <ISSUE-KEY>. Kept #$KEEP because <canonical reason>."
git update-ref -d "refs/tmp/pr-$KEEP"
git update-ref -d "refs/tmp/pr-$CLOSE"
```
If the duplicate has unique, useful commits, cherry-pick or manually port them onto the canonical branch, push that branch, then repeat the comparison before closing anything.
Record the cleanup in Linear before handoff:
- Canonical PR kept: `<PR URL>` with head SHA `<sha>`.
- Duplicate PR closed: `<PR URL>` with reason.
- Comparison evidence: command summary, for example `git log --left-right --cherry-pick` showed no unique commits in the duplicate.
- Any moved commits or remaining blockers.
Pattern from the SYM-92 incident: two agent launches produced overlapping PRs for the same Linear issue. The cleanup was to compare both heads, keep the PR that represented the active issue branch/final handoff, close the stale duplicate with a pointer to the kept PR, and record both PRs in Linear. Treat that as the reusable pattern; the kept PR is still selected by branch, head diff, and handoff evidence for the current issue.
## Validation Before PR
Run the smallest checks that prove the changed surface, plus the relevant merge gates:
```bash
# Always run for app or docs-visible app changes
pnpm --filter openhuman-app format:check
pnpm typecheck
# Focused app tests for changed TS/React behavior
pnpm --dir app exec vitest run <changed-test-files> --config test/vitest.config.ts
# Root Rust changes
cargo fmt --manifest-path Cargo.toml --all --check
# Tauri shell changes
cargo fmt --manifest-path app/src-tauri/Cargo.toml --all --check
```
For Rust behavior changes, prefer focused tests through the repo wrappers where available:
```bash
pnpm debug rust <test-filter>
```
If a command cannot run because the environment lacks vendored files or system packages, do not claim it passed. Copy the exact command and blocker into the PR body.
## PR Submission Checklist Preflight
Before handing off an AI-authored PR, validate the PR body locally. This catches unchecked template items before the GitHub workflow reports them.
For a generated PR body file:
```bash
pnpm pr:checklist /tmp/pr-body.md
```
For a generated body already loaded in an environment variable:
```bash
PR_BODY="$(cat /tmp/pr-body.md)" pnpm pr:checklist
```
For an existing GitHub PR:
```bash
gh pr view <number> --repo tinyhumansai/openhuman --json body --jq .body | pnpm pr:checklist -
```
Every checklist line must be checked. If an item does not apply, check it and include a short `N/A` reason on the same line. Do not leave `N/A` items unchecked.
## Refactor Parity Rules
For behavior extraction and architecture refactors:
- Identify the old guard order, fallback order, dispatch contract, or public API being preserved.
- Add focused parity tests when the behavior can be tested without broad integration setup.
- Do not reorder guards, fallback layers, RPC methods, or dispatch paths unless the issue explicitly asks for a behavior change.
- When adding a drift guard, verify it checks the source of truth as it exists in this repo. Do not assume generated strings are written literally in source files.
## PR Body Requirements
Every AI-authored PR must include:
- Linear issue key and URL.
- Branch name.
- Commit SHA.
- Files changed summary.
- Validation commands run.
- Validation commands blocked, with exact error text.
- Behavior intentionally changed, or `No intended behavior change`.
- Any duplicate/stale PRs that were closed or superseded.
## Review Before Handoff
Before handing off:
- Re-check GitHub CI status for the PR.
- Pull failed check logs before guessing.
- Fix format/type/test failures that are local to the PR.
- Leave broad system dependency or environment failures as explicit blockers.
- Update the Linear issue with PR URL, commit SHA, validations, and blockers.
-165
View File
@@ -1,165 +0,0 @@
# Cursor Cloud Agents — parallel workflow
Operator playbook for running 1520 [Cursor Cloud Agents](https://cursor.com/docs/cloud-agent) in parallel against OpenHuman. Companion to [`codex-pr-checklist.md`](codex-pr-checklist.md); the same merge gates apply.
This doc closes [`tinyhumansai/openhuman#1480`](https://github.com/tinyhumansai/openhuman/issues/1480).
## TL;DR
1. Write a **batch spec** — one JSON file naming N agents, their issues, branches, and owned paths.
2. **Validate** it (`pnpm agent-batch validate <spec>`) and **prove ownership disjointness** (`pnpm agent-batch overlap <spec>`).
3. Post **one launch comment** per agent (generated from the spec) into Cursor; each agent opens a branch and PR matching the spec.
4. Track progress with `pnpm agent-batch status <spec>` — markdown table of PR + CI per agent.
5. Pilot at N=3 before scaling to 1520.
Concretely, none of this is "Cursor magic" — it is a JSON contract + three small scripts that fail loudly if humans break it.
## Why a contract
Running N agents in parallel breaks in three ways:
- **Branch / PR collisions** — two agents picking the same branch name, or opening duplicate PRs against the same issue.
- **File collisions** — two agents editing the same module, producing conflicting merges.
- **Quality drift** — agents skipping format / typecheck / coverage and pushing red PRs.
The batch spec is the single source of truth that prevents the first two. The third is enforced by upstream CI ([`.github/workflows/pr-ci.yml`](../../.github/workflows/pr-ci.yml), [`.github/workflows/pr-quality.yml`](../../.github/workflows/pr-quality.yml)) — agents do not get to opt out.
## Batch spec
A batch is a JSON file living under `docs/agent-workflows/batches/` (gitignored — see [Privacy](#secrets-posture)) or generated ad hoc. Shape:
```json
{
"batch_id": "pilot-2026-05-15",
"base_repo": "tinyhumansai/openhuman",
"base_branch": "main",
"tracking_issue": 1480,
"agents": [
{
"id": "a01",
"issue": 1234,
"title": "short slug for the branch name",
"branch": "cursor/a01-1234-short-slug",
"owned_paths": ["app/src/features/foo/", "src/openhuman/foo/"],
"allowed_shared_paths": ["docs/TEST-COVERAGE-MATRIX.md"],
"labels": ["cursor-agent", "pilot"]
}
]
}
```
Field rules:
| Field | Required | Notes |
| ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `batch_id` | yes | Stable identifier — appears in PR bodies and the tracking comment. |
| `base_repo` | yes | Always `tinyhumansai/openhuman` unless explicitly delegated. |
| `base_branch` | yes | `main`. |
| `tracking_issue` | yes | One upstream issue per batch; that issue's comment thread is the dashboard (AC #6). |
| `agents[].id` | yes | Two-char + digits, e.g. `a01``a20`. Unique within batch. |
| `agents[].issue` | yes | Upstream issue number. **One issue per agent**, **one agent per issue.** |
| `agents[].branch` | yes | Must start with `cursor/` and contain the agent id and issue number. |
| `agents[].owned_paths` | yes | **Path prefixes** (directory ending in `/`) or exact files. **No globs.** Disjoint across agents — `overlap` enforces this. |
| `agents[].allowed_shared_paths` | no | Files the agent may touch even if another agent's prefix contains them (e.g. `docs/TEST-COVERAGE-MATRIX.md`, capability catalog). Best-effort only — overlap on these is **warned**, not blocked. |
| `agents[].labels` | no | PR labels. Always include `cursor-agent`. Add `docs` or `chore` to opt out of the soft `pr-quality` checks per [`pr-quality.yml`](../../.github/workflows/pr-quality.yml). |
### Why prefixes, not globs
A glob ownership model (`app/src/components/**`) is tempting but makes overlap detection ambiguous: does `app/src/**/*.test.ts` collide with `app/src/components/Foo/`? With prefixes the answer is mechanical: prefix containment in either direction = collision. CI files like `.github/workflows/*.yml`, the capability catalog at `src/openhuman/about_app/`, and similar shared surfaces should be assigned to **one** agent for the batch (or to no agent — the batch should be designed not to need them).
## Branch & PR conventions
- Branch off **`origin/main` (upstream)** at the moment the spec is written. Each agent fetches `origin/main` itself.
- Branch name format: `cursor/<id>-<issue>-<short-slug>` (e.g. `cursor/a04-1456-memory-namespace`). Enforced by `validate.mjs`.
- Push to the **forking remote the Cursor workspace is configured with**, not directly to `tinyhumansai/openhuman`. PRs are opened with `--head <fork-owner>:<branch>` against `tinyhumansai/openhuman:main`.
- **One PR per issue**, **one PR per branch**. If a retry is needed, update the existing PR; do not open a duplicate. Use the duplicate cleanup recipe in [`codex-pr-checklist.md`](codex-pr-checklist.md#duplicate-pr-cleanup).
- PR title: `<area>: <short imperative> (#<issue>)`. PR body **must** follow [`.github/PULL_REQUEST_TEMPLATE.md`](../../.github/PULL_REQUEST_TEMPLATE.md) verbatim, including the `## AI Authored PR Metadata` section.
- PR labels include at minimum `cursor-agent` and the batch id label `batch:<batch_id>` so the tracking comment can find them.
## Ownership boundaries
Disjointness rule: for any two agents A and B in the batch, no path prefix in `A.owned_paths` may be a prefix of any path in `B.owned_paths`, in either direction. `scripts/agent-batch/overlap.mjs` checks this and exits non-zero on a collision.
The rule applies to **prefixes**, not file existence. Two agents may own paths that don't exist yet (new modules), as long as no prefix contains another.
If two issues genuinely need the same module, **do not split them across agents**. Combine them into a single agent's scope or sequence the work.
## Quality gates
Agents run the same gates as any other PR. The launch comment instructs them explicitly — they do not get to drop any of these:
- **Format**: `pnpm --filter openhuman-app format:check`, `cargo fmt --manifest-path Cargo.toml --all --check`, and the Tauri shell equivalent if shell files changed.
- **Lint / typecheck**: `pnpm lint`, `pnpm typecheck`.
- **Tests (focused)**: targeted Vitest for changed files, focused Rust tests via `pnpm debug rust <filter>` for changed Rust.
- **Coverage**: agents must run `pnpm test:coverage` and `pnpm test:rust` locally and add tests for changed lines. The merge gate is `≥ 80% diff coverage`, enforced server-side by [`pr-ci.yml`](../../.github/workflows/pr-ci.yml). PRs below the threshold do not merge — agents that cannot reach the threshold must say so in the PR body, not paper over it.
- **PR checklist + coverage matrix**: [`pr-quality.yml`](../../.github/workflows/pr-quality.yml) checks the PR body and `docs/TEST-COVERAGE-MATRIX.md`. The `docs` and `chore` labels exempt a PR from these soft gates — use them only for PRs that genuinely change no behavior.
If the agent's environment cannot run a gate, the PR body must report the **exact command and error** under `### Validation Blocked`, not claim it passed. This is the same rule as the codex checklist.
## Secrets posture
Cursor Cloud Agents inherit env from the workspace. For OpenHuman, the cloud workspace MUST be configured with:
- **No** `STAGING_*` / `PRODUCTION_*` secrets.
- **No** `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or any other LLM provider key used by the production agent runtime — agents do code work, not LLM calls into production providers.
- A scoped `GITHUB_TOKEN` with `contents:write` and `pull_requests:write` on the **fork** the workspace pushes to, plus `pull_requests:write` on `tinyhumansai/openhuman` for PR creation. **No `admin:*`, no `actions:write`, no `secrets:*`.**
- `OPENHUMAN_APP_ENV` MUST be unset or set to `dev`. Never `staging` or `production` — staging writes `~/.openhuman-staging/core.token` referenced by [`AGENTS.md`](../../AGENTS.md) "Cursor Cloud specific instructions" and that token is **per-developer**, not for shared cloud workspaces.
- `.env.local`, `app/.env.local`, and `core.token` files are gitignored and must not be committed.
The agent's own environment is the smallest blast-radius surface. Production credentials are out of scope for code-writing agents.
## Progress visibility
One tracking issue per batch (`tracking_issue` in the spec). The launch script posts a **single comment** on that issue containing a markdown table generated by `scripts/agent-batch/status.mjs`:
| Agent | Issue | Branch | PR | CI | Coverage | Status |
| ----- | ----- | ------------------- | ------- | --------- | -------- | ------------ |
| a01 | #1234 | `cursor/a01-1234-…` | `#1234` | ✓ green | 87% | merged |
| a02 | #1235 | `cursor/a02-1235-…` | `#1235` | × failing | — | needs review |
Re-running `status` rewrites the same comment (looked up by a `<!-- batch:<id> -->` marker) so the issue thread doesn't fill with stale tables. The script reads:
- `gh pr list --repo tinyhumansai/openhuman --label batch:<id> --json …` for PR + state.
- `gh pr checks <pr>` for CI rollup.
- The `diff-coverage.md` artifact from `pr-ci.yml`, if downloaded — otherwise coverage shows `—`.
No external dashboard — GitHub issues + labels + the table are the single pane of glass.
## Pilot-then-scale
Do not launch 1520 agents on day one.
1. **N=3 pilot.** Pick three issues that are small (~200 LOC each), in three distinct domains, no overlap. Run the full flow: spec → validate → overlap → launch → status → merge.
2. **N=5 expansion.** Once the N=3 pilot has 3 green PRs (merged or at-review), expand to 5 in one batch. Watch CI queue times and rate limits.
3. **N=1520 production.** Only after two clean expansion batches. At this scale, watch `gh api rate_limit`, GitHub Actions concurrency, and Cursor's per-workspace agent limit.
If a batch surfaces a class of failure (e.g. agents inventing API names, agents skipping `cargo fmt`), fix the **launch comment template** that all agents inherit — don't fix it case-by-case.
## Operator quickstart
```bash
# 1. Draft the spec
cp docs/agent-workflows/pilot-batch-example.json /tmp/my-batch.json
$EDITOR /tmp/my-batch.json
# 2. Validate shape + naming
pnpm agent-batch validate /tmp/my-batch.json
# 3. Prove ownership disjointness
pnpm agent-batch overlap /tmp/my-batch.json
# 4. Generate one launch comment per agent (paste into Cursor)
pnpm agent-batch launch /tmp/my-batch.json --print-only
# 5. After agents have pushed, refresh the tracking comment
pnpm agent-batch status /tmp/my-batch.json --post
```
All scripts are in [`scripts/agent-batch/`](../../scripts/agent-batch/). They are zero-dep Node, executable from the repo root, and exit non-zero on policy violations so they are CI-friendly.
## Reference
- [`pilot-batch-example.json`](pilot-batch-example.json) — canonical example with 3 disjoint agents.
- [`scripts/agent-batch/`](../../scripts/agent-batch/) — validate / overlap / launch / status implementations + `node:test` suites.
- [`codex-pr-checklist.md`](codex-pr-checklist.md) — parent checklist; per-agent rules inherit from it.
- [`AGENTS.md`](../../AGENTS.md) and [`CLAUDE.md`](../../CLAUDE.md) — repo-wide rules every agent must follow.
@@ -1,151 +0,0 @@
# Open PR Reconciliation Handoff - 2026-05-13
Snapshot taken from `tinyhumansai/openhuman` on 2026-05-13 after fetching
`upstream/main` at `2b64ea8a` (`chore(release): v0.53.43`). The queue is
moving quickly; refresh the PR list before acting.
This is a reconciliation-first handoff. It does not change product code.
## Current Queue
- Open PRs: 28.
- Merge-state shape: 23 `BLOCKED`, 5 `DIRTY`.
- Draft PRs: #1644, #1519, #1420, #1383.
- High-risk conflict/rebase set: #1671, #1646, #1518, #1462, #1420.
- Broadest PRs by changed files: #1420 (93 files), #1518 (90 files), #1671
(19 files), #1519 (13 files), #1677 (12 files), #1488 (11 files).
| PR | State | Merge | Files | Size | Updated | Notes |
| --- | --- | --- | ---: | ---: | --- | --- |
| #1678 | ready | BLOCKED | 2 | +280/-16 | 2026-05-13 19:29Z | Triage prompt-guard rejection handling. |
| #1677 | ready | BLOCKED | 12 | +2623/-1 | 2026-05-13 19:25Z | Gameplay review workflow; failing broad CI and review requested. |
| #1676 | ready | BLOCKED | 4 | +238/-3 | 2026-05-13 18:54Z | Backend 4xx observability classifier. |
| #1672 | ready | BLOCKED | 2 | +68/-8 | 2026-05-13 18:07Z | Socket sustained-outage classifier; cancelled lightweight checks observed. |
| #1671 | ready | DIRTY | 19 | +1772/-265 | 2026-05-13 18:09Z | BYO Composio API key; conflicts with Composio/integrations/settings surfaces. |
| #1657 | ready | BLOCKED | 8 | +344/-1 | 2026-05-13 17:31Z | Gmail unsubscribe agent; overlaps tools/memory email surfaces. |
| #1656 | ready | BLOCKED | 8 | +52/-103 | 2026-05-13 15:13Z | Local AI test isolation. |
| #1646 | ready | DIRTY | 6 | +265/-20 | 2026-05-13 17:06Z | Portfolio readiness docs/lint; conflicts with active UI pages and docs. |
| #1645 | ready | BLOCKED | 2 | +39/-1 | 2026-05-13 18:25Z | Provider function argument parse guard. |
| #1644 | draft | BLOCKED | 9 | +1474/-0 | 2026-05-13 17:12Z | Deep-work automation scripts; draft. |
| #1641 | ready | BLOCKED | 3 | +458/-155 | 2026-05-13 20:02Z | Windows FS retry; changes requested. |
| #1636 | ready | BLOCKED | 2 | +212/-1 | 2026-05-13 18:04Z | Stale credentials lock recovery. |
| #1635 | ready | BLOCKED | 3 | +88/-31 | 2026-05-13 16:04Z | Screen-intelligence idempotent session start. |
| #1634 | ready | BLOCKED | 10 | +325/-51 | 2026-05-13 20:00Z | Agent max-iteration observability; checks still running. |
| #1633 | ready | BLOCKED | 10 | +344/-84 | 2026-05-13 20:08Z | Budget-exhausted observability; PR checklist failed and review requested. |
| #1632 | ready | BLOCKED | 7 | +474/-37 | 2026-05-13 20:07Z | Transient backend/integrations observability; checks still running. |
| #1630 | ready | BLOCKED | 2 | +298/-4 | 2026-05-13 18:28Z | Integrations local-AI URL fallback. |
| #1623 | ready | BLOCKED | 1 | +80/-0 | 2026-05-13 19:23Z | Vision RAM-tier observability skip. |
| #1620 | ready | BLOCKED | 1 | +57/-10 | 2026-05-13 17:59Z | UTF-8 memory ingest slicing. |
| #1589 | ready | BLOCKED | 3 | +377/-0 | 2026-05-13 14:42Z | Contributor reward invite automation. |
| #1561 | ready | BLOCKED | 3 | +744/-0 | 2026-05-13 18:38Z | Memory benchmark fixtures. |
| #1519 | draft | BLOCKED | 13 | +1065/-19 | 2026-05-12 07:07Z | Learning summarizer; draft. |
| #1518 | ready | DIRTY | 90 | +3727/-1295 | 2026-05-13 06:33Z | Chinese i18n; very broad UI surface. |
| #1488 | ready | BLOCKED | 11 | +646/-126 | 2026-05-13 18:09Z | Orchestrator delegation collapse. |
| #1462 | ready | DIRTY | 5 | +886/-789 | 2026-05-10 23:43Z | "Files Reviewed"; stale/conflicting, many failed checks, changes requested. |
| #1420 | draft | DIRTY | 93 | +10060/-35 | 2026-05-10 21:31Z | iOS client; largest branch, draft and conflicting. |
| #1383 | draft | BLOCKED | 1 | +57/-0 | 2026-05-10 21:30Z | Worker clone disposition docs. |
| #1321 | ready | BLOCKED | 4 | +84/-13 | 2026-05-10 21:31Z | Core-state rewards timeout UX. |
## Conflict Clusters
These files are touched by multiple open PRs and should be treated as hot
surfaces. Do not launch implementation work here until the relevant PRs are
rebased, merged, or closed.
| Surface | PRs | Risk |
| --- | --- | --- |
| `src/core/observability.rs` | #1676, #1634, #1633, #1632, #1623 | Highest overlap. Pick a canonical observability stack order before rebasing. |
| `app/src-tauri/src/lib.rs` | #1634, #1633, #1632, #1420 | Tauri shell overlap; avoid mixing iOS shell work with observability fixes. |
| `src/main.rs` | #1634, #1633, #1632 | Observability initialization overlap. |
| `src/openhuman/integrations/client.rs` | #1676, #1632, #1630 | Integrations error classification and local-AI fallback interact. |
| `app/src/App.tsx` | #1518, #1420 | Broad UI architecture conflict between i18n and iOS routing. |
| `app/src/pages/Settings.tsx` | #1671, #1420 | BYO Composio settings conflicts with iOS settings changes. |
| `src/core/all.rs`, `src/openhuman/about_app/catalog.rs`, `src/openhuman/mod.rs` | #1677, #1420 | Core module/catalog registration overlap. |
| `src/openhuman/agent/harness/*`, `src/openhuman/channels/runtime/dispatch.rs` | #1634, #1519, #1488 | Agent runtime, learning, and delegation work should not proceed independently. |
| `src/openhuman/credentials/profiles.rs` | #1641, #1636 | Windows retry and stale-lock recovery need one canonical credential-lock PR. |
| `Cargo.toml`, `Cargo.lock`, `pnpm-lock.yaml` | #1462, #1420 | Dependency/lockfile churn; #1462 appears stale and should not be used as a base. |
## Validation Gates
OpenHuman PRs carry both local and CI gates. For code PRs, the minimum gate is
the smallest focused command that proves the changed surface plus the relevant
global merge checks:
- PR body/template gate: `pnpm pr:checklist <body-file>`.
- Formatting: `pnpm --filter openhuman-app format:check`; Rust changes also
need `cargo fmt --manifest-path Cargo.toml --all --check` and, for Tauri,
`cargo fmt --manifest-path app/src-tauri/Cargo.toml --all --check`.
- TypeScript: `pnpm typecheck` for app-facing TypeScript changes.
- Focused tests: Vitest file-level tests for changed React/TS behavior; `pnpm
debug rust <filter>` for Rust behavior.
- Coverage gate: CI enforces changed-line diff coverage at or above 80%.
- Docs-only changes: run the PR checklist parser and any available markdown
link/check workflow locally when dependencies are installed.
Current blocker pattern:
- `DIRTY` PRs need rebase/conflict resolution before any CI result matters.
- `BLOCKED` PRs are mostly mergeable but waiting on checks, reviews, checklist
failures, or branch protection.
- #1677 and #1462 have concrete failed CI/check evidence and should not be
treated as safe to merge without repair.
- #1633 has a failed PR checklist and requested changes; fix the body/checklist
before spending compute on broad tests.
- Before rebasing or repairing any existing PR, refresh metadata with `gh pr
view <number> --repo tinyhumansai/openhuman --json
body,changedFiles,mergeStateStatus,mergeable,state,statusCheckRollup`.
- If validating an existing PR body, feed the actual body into the checklist
parser: `gh pr view <number> --repo tinyhumansai/openhuman --json body --jq
.body | pnpm pr:checklist -`.
## Recommended Reconciliation Order
1. Close or explicitly supersede stale/meta PRs before implementation:
#1462 should be audited first because it is conflicting, has broad failed
checks, and has an unclear title. If it has no unique current work, close it
with a pointer to the canonical replacement.
2. Reconcile observability as a single stack:
compare #1623, #1632, #1633, #1634, #1676, and #1672. Choose an order from
narrowest independent classifier to broadest runtime initialization. Rebase
one PR at a time and rerun focused observability tests plus checklist.
#1676 is the likely base candidate because it changes the backend 4xx
classifier that broader observability filters build on.
3. Reconcile credentials-lock PRs:
decide whether #1636 or #1641 is canonical, then port any unique tests/fixes
into the kept branch.
4. Keep broad feature branches out of the merge queue until smaller fixes land:
#1420, #1518, #1671, #1677, #1519, and #1488 all overlap hot surfaces.
5. Only after the queue is quieter, launch implementation work outside these
hot files.
Do not batch-rebase PRs from the conflict-cluster table. Rebase and validate one
branch at a time so failures can be attributed to the branch being repaired.
## Next Safe Implementation Slice
Do not start in observability, settings, Tauri shell, agent harness, or iOS/i18n
routing. The next safe slice is a small reconciliation task:
- Target: canonicalize #1462 disposition.
- Work:
- Refresh metadata first with `gh pr view 1462 --repo tinyhumansai/openhuman
--json body,changedFiles,mergeStateStatus,mergeable,state,statusCheckRollup`.
- Fetch `refs/pull/1462/head` and compare it against `upstream/main`.
- Identify whether the branch contains any unique, still-relevant code.
- Inspect lockfile/dependency churn in `Cargo.toml`, `Cargo.lock`,
`pnpm-lock.yaml`, and `scripts/mock-api-core.mjs`; these may have been
superseded by the `v0.53.43` release merge.
- If no unique work remains, close #1462 as stale/superseded and link this
handoff plus the canonical replacement PRs.
- If useful work remains, create a new narrow issue/branch for only that
surface; do not rebase the full PR.
- Validation:
- `git diff --name-status upstream/main...refs/tmp/pr-1462`
- `git log --left-right --cherry-pick --oneline upstream/main...refs/tmp/pr-1462`
- `gh pr view 1462 --repo tinyhumansai/openhuman --json body --jq .body |
pnpm pr:checklist -`
- `pnpm pr:checklist <generated-body>` only if a replacement PR is opened.
After #1462 is resolved, the next implementation-grade slice should be the
observability-stack canonicalization because it is blocking the most active
ready PRs.
-165
View File
@@ -1,165 +0,0 @@
# Agent prompt audit: grounding, tools, skills, MCPs
_Audit + remediation of the orchestrator and sub-agent system prompts, focused on
anti-hallucination discipline and de-duplication. Inspired by
[NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent)'s named,
reusable guidance blocks._
## Why this exists
The system prompts for the orchestrator and the ~27 sub-agents had accreted "slop":
the same anti-fabrication paragraph copy-pasted (and quietly drifting) across many
agents, specialist walkthroughs living in the wrong agent, a `## Safety` block that said
nothing about grounding, and an em-dash rule restated inline despite a global suffix
already banning it. There was **no single source of truth** for how an agent should treat
its tool list, skills, or missing context, so each agent re-invented it.
## How the prompt system assembles (reference)
- `SystemPromptBuilder` (`src/openhuman/agent/prompts/builder.rs`) renders an ordered
`Vec<Box<dyn PromptSection>>`, joins with `\n\n`, and appends `GLOBAL_STYLE_SUFFIX`.
- Concrete sections live in `src/openhuman/agent/prompts/sections.rs`.
- Three render paths produce a final system prompt:
1. **Static chain** (`SystemPromptBuilder::with_defaults` / `for_subagent`).
2. **Dynamic builders** (`from_dynamic`) — the orchestrator and ~25 other
`agents/<id>/prompt.rs` files each hand-assemble their body by calling the free
`render_*` helpers in `render_helpers.rs`. They funnel through
`SystemPromptBuilder::build()` for the final wrap.
3. **Narrow index-based renderer** (`render_subagent_system_prompt_with_format`) used by
the sub-agent runner for spawned sub-agents (does NOT go through `build()`).
- **Skills**: the orchestrator lists installed skills by name+desc; full SKILL.md bodies
are injected at *turn time* by `src/openhuman/workflows/inject.rs`, not in the system
prompt.
- **MCPs**: not injected into any agent prompt. OpenHuman's MCP server only exposes tools
to *external* clients; agents act through their own tool surface. There is no
agent-facing MCP catalogue section, by design.
- **KV-cache contract**: the system prompt is built once per session and frozen. Anything
in the cache-friendly prefix must be byte-stable (no time / RNG / host).
## Findings
| # | Finding | Resolution |
|---|---------|-----------|
| 1 | Anti-fabrication paragraph ("never invent ids; a tool not in your list does not exist") copy-pasted across crypto / markets / integrations / account-admin / mcp-setup / morning-briefing / researcher with no shared source. | Centralized into `GROUNDING_BODY` (one source of truth). |
| 2 | `## Safety` covered exfiltration / destructive commands but said nothing about grounding, not inventing tools/ids/files, or not ending a turn with a promise instead of an action. | Added the orthogonal grounding contract; kept `## Safety` for security. |
| 3 | Orchestrator `prompt.md` (235 lines) carried specialist content: a full Apple-Music click-sequence + keyboard walkthrough, and a ~47-line cron JSON walkthrough. | Moved to `desktop_control_agent` / `scheduler_agent`; orchestrator is now a lean routing table (175 lines). |
| 4 | Desktop-control instructions triple-duplicated across `SOUL.md`, orchestrator `prompt.md`, and `desktop_control_agent`. | `SOUL.md` + orchestrator trimmed to pointers; the worked example lives only in `desktop_control_agent`. |
| 5 | Em-dash rule restated inline in `orchestrator/prompt.md` although `GLOBAL_STYLE_SUFFIX` already bans em-dashes in every output. | Removed the inline restatement. |
| 6 | `omit_skills_catalog` rendering effect is inert (skills are agent-owned; the narrow renderer no longer emits a skills catalog). | **Retained.** See "Decisions" — the field is still live API surface (serde, CLI, 30+ TOMLs); removing it is a 60-site churn with no functional benefit. Documented as inert-but-retained. |
## The grounding contract
`GROUNDING_BODY` (`src/openhuman/agent/prompts/sections.rs`) is the canonical block.
It is deliberately **generic**: every clause must be true for *every* agent, including the
integrations executor. Agent-specific routing ("delegate external services", "pull slugs
from `composio_list_tools`") stays in that agent's own `prompt.md`.
```
## Grounding and tool use
- Your tools are exactly the ones listed in this prompt. You can only act through them.
If a capability is not one of your tools, say so plainly rather than pretending it exists.
- Never invent tool names, arguments, ids, slugs, file paths, URLs, chain ids, addresses,
quotes, metrics, or any other value. If you do not have it from a tool result or the user,
ask for it or look it up with a tool.
- Use your tools to act. Do not just describe what you would do and stop, and never end a
turn with a promise of future action: do it now, or hand back a concrete result.
- Never substitute plausible looking but fabricated output (made up data, invented file
contents, synthesised tool or API responses) for results you could not actually produce.
If a step failed, say it failed.
- Ground every factual claim in evidence you actually observed: a tool result, the user's
message, or cited memory. If the evidence is missing, partial, or truncated, say so or
fetch more instead of guessing.
- Skills run only via `run_workflow`, and only the skills listed as installed exist. Do not
invent skill ids.
```
The first, third, and fourth bullets mirror Hermes's `TOOL_USE_ENFORCEMENT_GUIDANCE`
("use tools to act, never end a turn with a promise") and `TASK_COMPLETION_GUIDANCE`
("never substitute fabricated output for results you couldn't produce").
### How it reaches every agent
Rather than splice it into all ~26 dynamic `prompt.rs` files (drift-prone), the contract
is appended at the two chokepoints that every prompt funnels through, just like
`GLOBAL_STYLE_SUFFIX`:
- `SystemPromptBuilder::build()` — covers the static chain and all dynamic agents.
- `render_subagent_system_prompt_with_format()` — covers spawned sub-agents.
Both reference the same `GROUNDING_BODY` const, so they can never drift. A `render_grounding()`
helper and a `GroundingSection` PromptSection are also exposed for explicit use. Placement is
near the tail (just before the output-style rules) so it reads as a closing contract; the
text is byte-stable, so it stays in the cache-friendly prefix.
Coverage is asserted by `grounding_contract_appended_to_every_build_path`
(`mod_tests.rs`), which checks the marker appears exactly once across the static, sub-agent,
and dynamic build paths, plus an assertion in the narrow-renderer test.
## Decisions / deviations from the original plan
- **No `omit_grounding` flag.** The contract is always on. It is universal
anti-hallucination guidance and the text is generic enough to be true for every agent, so
threading an opt-out through serde + every caller would be churn for zero benefit.
- **`omit_skills_catalog` retained, not removed.** Its rendering effect is inert, but the
field is wired into serde defaults, the agent CLI dump, and 30+ `agent.toml` files.
Removing it is a 60-site change with real regression surface and no functional payoff,
which is out of proportion for a prompt audit. Flagged here instead.
- **Cron delegate-vs-direct contradiction left in place.** The orchestrator decision tree
routes scheduling to `schedule_task`, while the (now compressed) scheduling rules still
let it call `cron_add` directly when that tool is in its list. Resolving which is
authoritative is a behavioral change beyond this audit; the load-bearing rules
(never via `run_workflow`; always confirm) were preserved.
## Touched files
- `src/openhuman/agent/prompts/sections.rs``GROUNDING_BODY` + `GroundingSection`.
- `src/openhuman/agent/prompts/builder.rs` — central append in `build()`.
- `src/openhuman/agent/prompts/render_helpers.rs``render_grounding()` + narrow-renderer append.
- `src/openhuman/agent/prompts/mod.rs` / `mod_tests.rs` — re-export + coverage tests.
- `src/openhuman/agent_registry/agents/{crypto_agent,markets_agent}/prompt.md` — drop generic clause.
- `src/openhuman/agent_registry/agents/orchestrator/prompt.md` — slimmed routing table.
- `src/openhuman/agent_registry/agents/{desktop_control_agent,scheduler_agent}/prompt.md` — received the moved walkthroughs.
- `src/openhuman/agent/prompts/SOUL.md` — trimmed duplicated desktop sequence.
## Verifying
```bash
GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml --lib agent::prompts::
GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml --lib agent_registry::agents
```
---
## Provider prompt-cache behaviour (#3939)
The byte-stable prompt prefix above is what makes KV-cache reuse possible. How
much of that reuse a user actually gets depends on the backend they route to.
`Provider::prompt_cache_capabilities()` (`src/openhuman/inference/provider/traits.rs`,
`PromptCacheCapabilities`) makes that contract explicit per provider:
| Provider | automatic prefix cache | reports cached input tokens | explicit cache-control | thread/session grouping |
|---|---|---|---|---|
| OpenHuman backend | ✓ | ✓ | — | ✓ (`thread_id` extension) |
| OpenAI / OpenRouter / GMI (OpenAI-compatible) | ✓ | ✓ | — | — (prefix identity) |
| Other / custom / LM Studio compatible | conservative default — all `false` | | | |
Notes:
- **Conservative by default.** Unknown or custom OpenAI-compatible slugs report
no caching, so we never assume cache hits or send cache-only request fields a
provider may not honour. Opting a provider in is a one-line edit to
`prompt_cache_for_compatible_slug` (`compatible.rs`) once verified.
- **Usage normalization is provider-agnostic.** `extract_usage` already folds the
OpenAI `usage.prompt_tokens_details.cached_tokens` shape and the
`openhuman.usage.cached_input_tokens` extension into
`UsageInfo.cached_input_tokens`, so cached-prefix cost accounting
(`src/openhuman/agent/cost.rs`) is exact wherever the provider reports it.
- **No OpenHuman-only leakage.** `explicit_cache_control` stays `false` for every
OpenAI-compatible provider (the chat-completions API has no such field), and
`thread_id` grouping is declared only on `OpenHumanBackendProvider`.
Follow-ups (not in this slice): explicit cache-control request shaping for
providers that support it (e.g. Anthropic `cache_control`), a `ChatRequest`
cache-boundary marker, and extending cached-token parsing to non-OpenAI usage
shapes (e.g. DeepSeek `prompt_cache_hit_tokens`).
-204
View File
@@ -1,204 +0,0 @@
# iOS Client Setup
This document covers everything a developer needs to build, run, and test the OpenHuman iOS client.
---
## Prerequisites
- macOS 14+ with Xcode 15.4+
- iOS 17+ physical device or simulator
- Rust toolchain with `aarch64-apple-ios` target
- pnpm (version pinned in root `package.json`)
- Apple Developer account with a provisioning profile
```bash
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
```
---
## Initial setup
Run the helper script from the repo root. It calls `tauri ios init` with the correct working directory and prints next steps.
```bash
bash scripts/ios-init.sh
```
`tauri ios init` scaffolds `app/src-tauri-mobile/gen/apple/`. That directory is **gitignored** (it contains bundle-identifier-specific Xcode project files that differ per developer account).
### Info.plist privacy keys
`tauri ios init` creates a generated `Info.plist` at:
```
app/src-tauri-mobile/gen/apple/<bundle-id>_iOS/Info.plist
```
`scripts/ios-init.sh` injects these privacy keys into the generated plist:
```xml
<key>NSCameraUsageDescription</key>
<string>OpenHuman uses the camera to scan the pairing QR code from your desktop.</string>
<key>NSMicrophoneUsageDescription</key>
<string>OpenHuman uses the microphone for push-to-talk voice messages.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>OpenHuman uses on-device speech recognition to transcribe your voice messages.</string>
```
---
## Development workflow
```bash
# Start the iOS dev build (hot-reload via Vite, deployed to simulator or device):
pnpm tauri:ios:dev
# From the repo root:
pnpm tauri:ios:dev
```
The `tauri:ios:dev` script uses `@tauri-apps/cli@^2` directly (via `npx --package`), **not** the vendored CEF-aware CLI. The CEF CLI is only needed for the desktop build.
Set your development team in Xcode (generated project > Signing & Capabilities) before deploying to a physical device.
---
## Production build
```bash
pnpm tauri:ios:build
# or from repo root:
pnpm tauri:ios:build
```
---
## App Store Connect delivery
`.github/workflows/ios-appstore.yml` builds a signed `iphoneos` archive, exports an IPA, uploads the IPA to App Store Connect/TestFlight with `altool`, and stores the IPA + dSYMs as GitHub Actions artifacts.
Run it from GitHub Actions > iOS App Store. Inputs:
- `ref` -- optional git ref to build.
- `build_number` -- optional `CFBundleVersion`; defaults to `github.run_number`.
- `upload_to_app_store_connect` -- set `false` for a signed archive/export dry run.
Required GitHub environment: `App-Store`.
Required secrets:
- `APPLE_TEAM_ID` -- Apple Developer Team ID.
- `IOS_KEYCHAIN_PASSWORD` -- temporary CI keychain password.
- `IOS_DISTRIBUTION_CERTIFICATE_BASE64` -- base64-encoded `.p12` Apple Distribution certificate.
- `IOS_DISTRIBUTION_CERTIFICATE_PASSWORD` -- password for that `.p12`.
- `IOS_APPSTORE_PROVISIONING_PROFILE_BASE64` -- base64-encoded App Store provisioning profile for `com.tinyhumansai.openhuman`.
- `APP_STORE_CONNECT_API_KEY_ID` -- App Store Connect API key ID.
- `APP_STORE_CONNECT_ISSUER_ID` -- App Store Connect issuer ID.
- `APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64` -- base64-encoded `AuthKey_<key id>.p8`.
Local encoding helpers:
```bash
base64 -i ios_distribution.p12 | pbcopy
base64 -i OpenHuman_AppStore.mobileprovision | pbcopy
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy
```
The workflow uploads a build to App Store Connect. It does not submit the build for App Review; that remains a deliberate App Store Connect action.
### Local upload script
After downloading an App Store provisioning profile and App Store Connect API key, you can build/export/upload from this Mac:
```bash
TEAM_ID=XXXXXXXXXX \
IOS_APPSTORE_PROVISIONING_PROFILE_PATH=/path/to/OpenHuman_AppStore.mobileprovision \
ASC_KEY_ID=XXXXXXXXXX \
ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
ASC_KEY_PATH=/path/to/AuthKey_XXXXXXXXXX.p8 \
UPLOAD=1 \
scripts/ios-appstore-upload.sh
```
Use `UPLOAD=0` to stop after IPA export.
### Updating without a new App Store build
iOS cannot self-update native code or replace the installed app binary outside App Store/TestFlight distribution. For OpenHuman, this means changes to Rust, Tauri plugins, native permissions, bundled frontend code, or the app shell need a new reviewed build.
Safe server-side updates include model/provider configuration, feature flags, prompt/content changes, remote data, and backend behavior that the shipped client already knows how to render. Be conservative with remote JavaScript or plugin-style features: Apple allows some software/content delivered outside the binary under specific rules, but it must stay within App Review limits and must not expose native platform APIs without permission.
---
## Pairing flow
```
Desktop iOS
| |
|-- Settings > Devices > "Pair" |
|-- devices_create_pairing RPC |
| (backend issues channelId, |
| pairingToken, sessionToken) |
|-- QR shown |
| scan QR --------|
| (extract cid, |
| pt, cpk, rpc?) |
| iOS connects |
| to backend |
| tunnel:connect |
| (role:client, |
| channelId, |
| pairingToken) |
| backend returns |
| iOS sessionToken|
| X25519 handshake|
| over tunnel |
|<-- DevicePaired event |
|-- device appears in Devices list |
```
Transport selection (handled by `TransportManager`):
1. LAN HTTP -- fast, zero-latency, requires same network.
2. Socket.io tunnel -- E2E encrypted via XChaCha20-Poly1305 over X25519 key agreement.
3. Cloud HTTP -- fallback when LAN and tunnel are unreachable.
---
## Security notes
- The tunnel backend is a **blind forwarder**. It never sees plaintext payloads.
- `pairingToken` is single-use and hashed at rest on the backend.
- `sessionToken` is per-peer, revocable from the desktop Devices panel.
- X25519 key agreement runs on first connect; the derived symmetric key is stored in-memory for the session.
- **TODO (follow-up PR):** migrate the iOS symmetric key to the iOS Keychain for persistence across app restarts without re-pairing.
---
## Known limitations
- Single backend instance only (no multi-region failover).
- No APNs push notifications -- app must be foregrounded for real-time delivery.
- Event-driven pairing detection on the desktop side uses 2-second polling until an SSE/socket event bridge lands.
---
## CI
The `.github/workflows/ios-compile.yml` workflow runs as an iOS compile sanity check. It provides:
- **Hard gate:** `cargo check` on the iOS target for `app/src-tauri-mobile` and a host-target check for `packages/tauri-plugin-ptt`.
- **Hard gate:** TypeScript compile (`pnpm compile`).
- **Hard gate:** iOS-related Vitest suites.
Full signed App Store builds run through `.github/workflows/ios-appstore.yml`.
---
## Backend dependency
The tunnel transport requires `tinyhumansai/backend#709` to be merged and deployed before end-to-end pairing works. The `devices_create_pairing` RPC will return a tunnel registration error until that backend is live.
-24
View File
@@ -1,24 +0,0 @@
# App Review Information Draft
Fastlane uploads `fastlane/metadata/review_information/` automatically when that directory exists. Keep draft review details outside the Fastlane metadata tree until the real review contact email and phone number are ready.
## Contact
- First name: Steven
- Last name: Enamakel
- Email: TODO
- Phone: TODO
## Notes
OpenHuman for iPhone is a companion app for the OpenHuman desktop runtime.
To review the app:
1. Install and open the submitted iOS build.
2. Install and open the OpenHuman desktop app on macOS, Windows, or Linux.
3. In the desktop app, open Settings > Devices and choose Pair phone.
4. Use the iPhone app to scan the pairing QR code.
5. After pairing, test text chat and push-to-talk voice input from the iPhone.
The iOS app requires camera permission for QR pairing and microphone permission for push-to-talk voice input. It does not function as a standalone assistant without a paired OpenHuman desktop runtime.
-104
View File
@@ -1,104 +0,0 @@
# App Store Listing
This is the prepared product-page kit for the OpenHuman iOS companion app.
## App Store Connect Fields
- App name: `OpenHuman`
- Subtitle: `AI companion for your desktop`
- Bundle ID: `com.tinyhumansai.openhuman`
- SKU: `com.tinyhumansai.openhuman`
- Primary category: `Productivity`
- Secondary category: `Utilities`
- Copyright: `2026 Tiny Humans AI`
- Support URL: `https://tinyhumans.ai/openhuman`
- Marketing URL: `https://tinyhumans.ai/openhuman`
- Privacy Policy URL: `https://tinyhumans.gitbook.io/openhuman/legal/privacy-policy`
Metadata files live in `fastlane/metadata/en-US/`.
## Description
Use `fastlane/metadata/en-US/description.txt`.
## Keywords
Use `fastlane/metadata/en-US/keywords.txt`.
## App Icon
The App Store icon is already included in the iOS build:
- `app/src-tauri-mobile/icons/store/appstore.png`
- `app/src-tauri-mobile/icons/ios/AppIcon.appiconset/1024.png`
Both are 1024 x 1024 PNG assets.
## Screenshots
Generate the 6.9-inch iPhone screenshot set:
```bash
pnpm --dir app exec node ../scripts/ios-appstore-assets.mjs
```
Upload the generated files from:
```text
fastlane/screenshots/en-US/
```
Use these in App Store Connect under the iPhone 6.9-inch display screenshot slot.
You can also push the generated screenshots with Fastlane:
```bash
ASC_KEY_ID=9KD934428C \
ASC_ISSUER_ID=69a6de8b-cc07-47e3-e053-5b8c7c11a4d1 \
ASC_KEY_PATH=/Users/enamakel/Downloads/AuthKey_9KD934428C.p8 \
ASC_APP_VERSION=1.0 \
fastlane ios push_screenshots
```
Metadata can be pushed with the App Store Connect API helper:
```bash
ASC_KEY_ID=9KD934428C \
ASC_ISSUER_ID=69a6de8b-cc07-47e3-e053-5b8c7c11a4d1 \
ASC_KEY_PATH=/Users/enamakel/Downloads/AuthKey_9KD934428C.p8 \
ASC_APP_ID=6761229174 \
scripts/ios-appstore-metadata.mjs
```
## App Review Notes
Use `fastlane/metadata/review_information/notes.txt`.
Before submitting, replace the TODO contact fields in:
- `fastlane/metadata/review_information/email_address.txt`
- `fastlane/metadata/review_information/phone_number.txt`
## Privacy Answers Draft
Use this as the starting point for App Privacy in App Store Connect:
- Camera: used to scan the desktop pairing QR code.
- Microphone: used for push-to-talk voice messages.
- Speech recognition: used to transcribe voice messages.
- Identifiers/session data: pairing/session data may be used to connect the phone to the paired OpenHuman desktop runtime.
- User content: messages and voice transcripts are sent to the paired OpenHuman runtime to provide assistant responses.
Before submitting, make sure the App Privacy answers match the production backend/runtime behavior.
## Current Apple Requirements Checked
Apple currently requires one to ten screenshots for each platform localization, accepts `.jpeg`, `.jpg`, and `.png`, and allows high-resolution iPhone screenshots to scale down to smaller sizes. Apple lists the 6.9-inch portrait sizes as accepted high-resolution iPhone screenshot sizes.
Apple currently limits:
- App name: 30 characters
- Subtitle: 30 characters
- Promotional text: 170 characters
- Description: 4000 characters
- Keywords: 100 bytes
-47
View File
@@ -1,47 +0,0 @@
# OpenHuman iOS Privacy Policy Draft
Last updated: June 6, 2026
OpenHuman for iPhone is a companion app for the OpenHuman desktop runtime. It lets you pair your phone with your desktop OpenHuman app, send text messages, and use push-to-talk voice input.
The App Store Connect listing uses the published TinyHumans privacy policy:
https://tinyhumans.gitbook.io/openhuman/legal/privacy-policy
This draft is retained as iOS-specific reference material for review notes and privacy-answer preparation.
## Data The App Uses
OpenHuman for iPhone may process:
- Pairing data, such as a short-lived QR pairing token and device connection identifiers.
- Messages you send to your paired OpenHuman desktop runtime.
- Voice transcripts created from push-to-talk input.
- Basic connection state needed to route requests to the paired desktop runtime.
## Permissions
The iOS app requests:
- Camera access to scan the desktop pairing QR code.
- Microphone access for push-to-talk voice messages.
- Speech recognition access to transcribe push-to-talk messages.
## How Data Is Used
Data is used to:
- Connect your phone to your paired OpenHuman desktop runtime.
- Send your messages and voice transcripts to the paired runtime.
- Receive assistant responses from the paired runtime.
- Maintain connection health while the app is in use.
## Desktop Runtime
The iPhone app is not a standalone assistant. Your long-term workspace, memory, configuration, and integrations are managed by your OpenHuman desktop setup. The exact data handling behavior depends on how you configure OpenHuman desktop, including whether you use managed services, local providers, or custom providers.
## Contact
For support, visit:
https://tinyhumans.ai/openhuman
-181
View File
@@ -1,181 +0,0 @@
# Skills Runner Unification
**Status**: Proposed — implementation in progress on `run/codegraph-full`.
**Owner**: codegraph-skills track.
**Related**: PR #2802 (dev-workflow active-config UX), PR #2864 (smart issue selection / cron prompt embedding), the `feat/codegraph-skills` series that introduced `SkillsRunnerBody`.
---
## TL;DR
We currently maintain **two parallel scheduling UIs** for the same underlying primitive (`openhuman.cron_*` RPCs against an agent `prompt`):
1. **`SkillsRunnerBody`** (generic, every bundled skill) — picks any skill, dynamically renders inputs from `openhuman.skills_describe`, can run-now or save-as-cron. Lists saved schedules as a read-only summary with run/remove buttons.
2. **`DevWorkflowPanel`** (dev-workflow-only) — bespoke smart-issue-picker UI for `dev-workflow`, with an "active configuration" card, enable/disable toggle, embedded run history with per-run output viewer.
`DevWorkflowPanel` is just a hard-coded specialization of what `SkillsRunnerBody` already does — same cron RPC, same `agent` job type, same prompt-template scheme. The polish that lives in `DevWorkflowPanel` (toggle, run-history-with-output, active-config card) should be lifted into `SkillsRunnerBody`, and the dev-workflow-specific GitHub issue picker should be extracted as a conditionally-mounted sub-component. Then `DevWorkflowPanel` retires.
---
## Current state
### `app/src/components/skills/SkillsRunnerBody.tsx` (990 lines)
Generic skill runner used by both Settings → Developer Options → Skills Runner AND the `/skills` "Runners" tab.
| Concern | Location |
| --- | --- |
| Per-skill cron-name prefix | line 94 (`CRON_NAME_PREFIX = 'skill-run-'`) |
| Cron-name builder (skill + inputs hash) | lines 100-113 |
| Generic agent-prompt builder (re-fires `skill_run` at tick) | lines 116-127 |
| `scheduledJobs` state (filtered by name prefix) | line 233 |
| Saved-schedule render list | lines 828-875 |
| Per-schedule actions | `handleRunJobNow` (526-536), `handleRemoveJob` (538-548) — **no enable/disable** |
| Recent-runs viewer (cross-skill or scoped) | lines 882-985 |
| Inline log viewer (per run, auto-tail) | lines 440-519 |
### `app/src/components/settings/panels/DevWorkflowPanel.tsx` (792 lines)
| Concern | Location |
| --- | --- |
| `dev-workflow-`-prefixed cron-name | line 376 |
| Fork detection via Composio | lines 190-303 |
| Branch dropdown | lines 707-732 |
| Embedded skill-instructions prompt | lines 338-373 |
| **Active-config card** (rendered at top when any job exists) | lines 486-647 |
| **Enable/disable toggle** | lines 433-444 + render at 502-516 |
| **Run-history rows with per-run expandable output** | lines 591-645 (render), 306-322 (fetch via `openhumanCronRuns`) |
| **`last_output` viewer** | lines 580-589 |
### What's NOT in `SkillsRunnerBody` today
- Enable/disable toggle per schedule (the cron RPC supports `openhumanCronUpdate(id, { enabled })` — DevWorkflowPanel:439 — already wired generically).
- Per-schedule run history (`openhumanCronRuns(jobId, limit)` exists in `cron.ts`; SkillsRunnerBody currently shows recent runs only via `skillsApi.recentRuns()` which scans the skill log directory, not the cron `runs` table).
- "Active config card" — surface the most-recently-active schedule prominently rather than as one row in a list.
- Anything dev-workflow-specific (the GitHub repo / fork / branch picker workflow with smart auto-detection).
---
## Why unify
1. **UX symmetry.** Users with multiple `dev-workflow` schedules across different repos (a real use case Cyrus's PR #2802 was prepping for) need exactly the same UI as users with multiple `pr-review-shepherd` schedules across different repos. Today there's a privileged surface for one skill.
2. **Cron RPCs are already generic.** `openhuman.cron_update`, `cron_runs`, `cron_run` all take a `job_id` — no skill-specific logic in the core. We have zero RPC work to do.
3. **`DevWorkflowPanel` is a hard-coded specialization.** It bakes `dev-workflow-` into the cron name and the prompt template into the file. Both are anti-patterns once the generic runner exists: `SkillsRunnerBody` already builds prompts that re-fire `skill_run`, which routes through the skill registry and gets the up-to-date `system.md`/inputs without re-deploying the panel.
4. **Smaller surface to maintain.** Two views means two test suites, two i18n key namespaces (`settings.devWorkflow.*` and `settings.skillsRunner.*`), two render bugs to fix. Cyrus shipped 8 fixes to DevWorkflowPanel in 4 weeks; every one would have benefited the generic runner if they'd shared code.
---
## Unified information architecture
```
/skills
├─ Library tab (existing)
└─ Runners tab (existing — this is where SkillsRunnerBody lives)
Runners tab body (SkillsRunnerBody, after unification):
┌─ Skill picker ─────────────────────────────────────────────┐
│ Skill: [ dev-workflow ▾ ] │
└────────────────────────────────────────────────────────────┘
┌─ Saved schedules for this skill ───────────────────────────┐
│ │
│ ★ ACTIVE dev-workflow-tinyhumansai-openhuman │
│ every 2 hours · next run in 23m │
│ last: ok · 47s ago [⏵ toggle] │
│ [Run now] [▾ history (5)] [Remove] │
│ ▾ history │
│ 2026-05-29 13:01 ok 51s │
│ <expandable per-run output pre> │
│ 2026-05-29 11:01 ok 49s │
│ │
│ ────────────────────────────────────────────────────── │
│ │
│ ○ dev-workflow-graycyrus-openhuman │
│ daily @ 9am · paused │
│ last: error · 4h ago [⏵ toggle] │
│ [Run now] [▾ history] [Remove] │
│ │
│ [ + Add new schedule for dev-workflow ] │
└────────────────────────────────────────────────────────────┘
┌─ Configure & run ──────────────────────────────────────────┐
│ [skill description — what_to_use] │
│ │
│ Inputs: │
│ <schema-driven form, today> │
│ │
│ When skill_id === 'dev-workflow', ALSO render: │
│ <SmartIssuePicker subcomponent — fork detection, │
│ GitHub-connected repo dropdown, branch dropdown> │
│ │
│ [Run now] [Save as schedule …] │
└────────────────────────────────────────────────────────────┘
┌─ Recent runs (skill-scoped) ───────────────────────────────┐
│ (existing — unchanged; reads skill_log scan) │
└────────────────────────────────────────────────────────────┘
```
Notes:
- The **★ ACTIVE** treatment lifts DevWorkflowPanel's "active configuration" pattern but generalises it: any enabled schedule gets the same emphasis. If multiple are enabled, the most recently run one is "active" — first in the list, larger card.
- The "+ Add new schedule" affordance just reveals the existing inputs form (we don't need a second form; we just gate the existing one behind a disclosure so the saved-schedules list reads cleaner).
- Run history per schedule uses `openhuman.cron_runs` (the same RPC DevWorkflowPanel already uses) — this is *separate* from "Recent runs" at the bottom which uses `skillsApi.recentRuns()` scanning the skill log directory. Both have value: cron-run history is structured per-schedule with status/duration; skill log scan catches run-now invocations that don't go through a schedule.
---
## Migration / deprecation path
### Phase 1 — Design doc (this file).
### Phase 2 — Smoke prototype.
- Add enable/disable toggle to existing `scheduledJobs.map(...)` row in `SkillsRunnerBody.tsx`.
- Mirror `openhumanCronUpdate(jobId, { enabled: !job.enabled })` from DevWorkflowPanel:439 exactly.
- New i18n keys: `settings.skillsRunner.scheduleEnabled`, `settings.skillsRunner.scheduleDisabled` (full 12-locale chunk parity).
- Vitest unit coverage for toggle.
### Phase 3 — Full incremental implementation (one commit per chunk).
1. Per-schedule run-history + expandable output viewer (port DevWorkflowPanel:593-635).
2. Active-config card pattern (port DevWorkflowPanel:502-547).
3. Extract `SmartIssuePicker` subcomponent into `app/src/components/skills/SmartIssuePicker.tsx`; conditionally render in `SkillsRunnerBody` when `skillId === 'dev-workflow'` (with a TODO for a schema-driven `[input].picker = "github-issue"` upgrade — see Open questions).
4. Deprecate `DevWorkflowPanel`: replace its body with a one-line "moved to /skills" notice + nav link; OR strip it from the Settings nav and delete the file. Update or remove `DevWorkflowPanel.test.tsx` accordingly. Decision in Phase 3 chunk 4 once we see whether the Settings nav strip is clean.
5. i18n parity audit (`pnpm i18n:check`) — fold any new keys into all 12 non-English chunk files.
### Phase 4 — Verification.
- Playwright via CDP `http://127.0.0.1:19222` against the running dev app on Vite port 1428.
- Confirm: schedule toggle + expand work; smart-issue picker shows only for `dev-workflow`; switching to `github-issue-crusher` / `pr-review-shepherd` shows the generic form.
- Save `G:/tmp/oh-skills-unified.png`.
- Run `pnpm typecheck` + `pnpm debug unit` on changed files.
---
## Risk + test plan
### Risks
| Risk | Mitigation |
| --- | --- |
| **Breaking existing dev-workflow users** — they have a configured cron job named `dev-workflow-<repo>`; the new generic runner uses `skill-run-<skillId>-<hash>` as the prefix. | Filter saved-schedules list by **both** prefixes when `skillId === 'dev-workflow'` so existing jobs surface. Don't migrate names — they keep working. |
| **i18n drift** — there's already significant pre-existing drift in `en-N.ts` chunks for `settings.skillsRunner.*` keys (audit log shows ~50 keys in `en.ts` not in chunks). | Address chunk drift for the **new** keys this PR adds (toggle, history, smart-picker). Pre-existing drift is out of scope but worth a follow-up. |
| **`DevWorkflowPanel` removal breaks the deep link or settings nav route.** | Check `settings/AppSettings.tsx` (or wherever the nav is wired) and either preserve the route as a redirect to `/skills` or update the nav entry. |
| **Coverage gate (≥80% on changed lines)** on a 990-line component. | Add focused tests per Phase 3 chunk: toggle test (Phase 2), history-expand test (chunk 1), active-card render test (chunk 2), conditional-picker test (chunk 3). |
| **Stale-component test deletion regressing dev-workflow coverage.** | If `DevWorkflowPanel.test.tsx` is deleted, ensure equivalent coverage exists in `SkillsRunnerBody.test.tsx` for the smart-picker path. |
### Test plan
Per phase commit:
- **Phase 2**: `SkillsRunnerBody.test.tsx` — render one saved job, click toggle, assert `openhumanCronUpdate` called with `{ enabled: false }`, assert refresh-list invoked.
- **Phase 3.1**: render one saved job with `runHistory`, click expand, assert per-run output `<pre>` visible. Assert `openhumanCronRuns(jobId, 5)` called on render.
- **Phase 3.2**: render multiple jobs, assert most-recent-active sorted to top with `data-testid="active-schedule"` (or equivalent). Assert non-enabled jobs sorted below.
- **Phase 3.3**: render with `skillId === 'dev-workflow'`, assert `SmartIssuePicker` present. Render with `skillId === 'github-issue-crusher'`, assert it's absent. Subcomponent's own tests cover Composio loading/error paths (move from `DevWorkflowPanel.test.tsx`).
- **Phase 3.4**: if DevWorkflowPanel becomes a redirect, assert nav still routes correctly; if deleted, delete the test file.
---
## Open questions
1. **Schema-driven pickers vs. hard-coded `if (skillId === 'dev-workflow')`.** The clean answer is to extend `skill.toml`'s `[[inputs]]` schema with an optional `picker = "github-issue"` field, and let the runner route to a `SmartIssuePicker` subcomponent based on the picker key. The shortcut answer is the hard-coded `if`. Phase 3 chunk 3 ships the shortcut with a `TODO(picker-schema)` comment. The schema upgrade is a follow-up issue worth filing — it would also benefit `RepoPicker` and `BranchPicker`, which today route by *name convention* (`REPO_INPUT_NAMES` / `BRANCH_INPUT_NAMES` sets in SkillsRunnerBody:42-55), which is brittle.
2. **One enabled schedule per (skill, inputs) combo, or many?** Today DevWorkflowPanel allows only one (it looks up `name?.startsWith('dev-workflow')` and updates in place). `SkillsRunnerBody` allows many (the cron-name hash includes input values, so two different repos produce two jobs). The unified UX should keep "many" — but display only one as the prominent "ACTIVE" card. Pre-existing `dev-workflow-<repo>` jobs will surface in the list once we add the dual-prefix filter.
3. **Run history retention.** DevWorkflowPanel pulls last 5; SkillsRunnerBody's bottom "Recent runs" scans last 10 log files. Unify on a single source? For now, keep both — the per-schedule history is structured cron data, the bottom list is a cross-cutting "what ran lately" surface useful even with no schedules. Worth re-evaluating once both surfaces are in production for a few weeks.
4. **`last_output` field on `CoreCronJob` vs. per-run output on `CoreCronRun`.** Today both exist (`cron.ts:42-43` and `cron.ts:51`). DevWorkflowPanel renders `existingJob.last_output` in the active card AND per-run `run.output` in history rows. After unification, drop the duplicated `last_output` block; the per-run history already shows the most recent run's output. Lightweight change.
5. **Deprecation timing for the Settings → Developer Options → Dev Workflow nav entry.** Strip immediately (Phase 3 chunk 4), or leave as a redirect for one release? Leaning toward strip — the user is the maintainer, the panel is dev-only, and `/skills` is more discoverable than a buried Settings sub-page.
@@ -1,675 +0,0 @@
# Operator MVP - OpenHuman Fork Execution Plan
**Status:** Draft execution plan
**Date:** 2026-05-11
**Primary repo:** `openhuman`
**Reference repo:** `inbox`
**Goal:** Turn the fork into a phone-reachable personal operator that observes work, proposes next actions, asks for approval, acts through trusted connectors, verifies outcomes, and records evidence.
> **For agentic workers:** This is a planning lane artifact. Do not start broad implementation from chat context. Turn selected slices into Linear issues with acceptance criteria, branch names, owned files, validation commands, and PR handoff requirements.
---
## Product Thesis
The product is not a generic desktop assistant.
The product is an operator loop:
```text
Observe -> Propose -> Approve -> Act -> Verify -> Remember
```
Every feature should attach to one of those stages. Anything that does not make that loop faster, safer, or more useful is backlog.
The user-facing promise:
```text
Connect your real work once. The operator finds what needs attention, drafts useful next actions, asks before doing anything risky, executes through the right account/tool, and shows evidence that the work is done.
```
---
## Why This Can Beat The Existing Shape
OpenHuman already has a strong desktop shell, Rust core, controller registry, Composio integration, local memory, skills, screen/voice surfaces, and a growing capability catalog. The current weakness is product focus: many capabilities exist, but the user still has to decide what to ask, where to ask it, and how to know whether the agent actually finished.
The inbox repo already solved the more important product primitive: normalize noisy sources into work to do. It has concrete patterns for:
- local-first source adapters
- normalized thread/item records
- sync state
- source-of-truth routing
- confirmation-gated tools
- narrow MCP tools
- "Now", "Actionable", "Waiting On", and calendar-attached work views
The fork should combine them by making OpenHuman the operator runtime and inbox logic the work-discovery/control plane.
---
## Existing Assets To Reuse
### From OpenHuman
- Tauri desktop app and Rust sidecar runtime.
- Controller registry in `src/core/all.rs`.
- Domain layout under `src/openhuman/<domain>/`.
- Composio controllers in `src/openhuman/composio/`.
- Frontend Composio API wrappers in `app/src/lib/composio/`.
- Skills/runtime surfaces for app actions.
- Memory store and memory tree ingestion/retrieval.
- Capability catalog in `src/openhuman/about_app/`.
- Channels/controllers surface for external channels.
- Voice, screen intelligence, provider surfaces, notifications, and subconscious/background work scaffolding.
### From Inbox
- `message_index_store.py`: operational index with items, threads, sync state, actionability, urgency, open loops, summaries, and sender stats.
- `tools_registry.py`: one registry driving readonly and write-capable tools, with confirmation-gated writes.
- `CONNECTOR_ROADMAP.md`: source adapters -> normalization -> policy -> intent tools.
- `PLAN.md`: organize around work to do, not raw providers.
- `MCP_V1_PLAN.md`: public MCP/chat surface -> private local server, with token gates and confirmation gates.
- Google account/source-of-truth routing rules.
- Gmail, Calendar, Drive, Sheets, Notes, Reminders, GitHub, and iMessage connector experience.
---
## Core Architecture
### New Rust Domain
Create a dedicated domain:
```text
src/openhuman/operator/
|-- mod.rs
|-- types.rs
|-- store.rs
|-- policy.rs
|-- preflight.rs
|-- evidence.rs
|-- goal_packs.rs
|-- inbox_adapter.rs
|-- schemas.rs
|-- ops.rs
`-- ops_tests.rs
```
The domain owns operator state and business logic. React should render operator state and call controllers; it should not decide routing, safety, approval, or source-of-truth behavior.
### Controller Surface
Expose controllers through the existing registry:
```text
openhuman.operator_list_goal_packs
openhuman.operator_create_goal_pack
openhuman.operator_update_goal_pack
openhuman.operator_list_work_items
openhuman.operator_propose_next_actions
openhuman.operator_preview_action
openhuman.operator_create_approval_request
openhuman.operator_approve_request
openhuman.operator_reject_request
openhuman.operator_execute_approved_action
openhuman.operator_record_evidence
openhuman.operator_list_evidence
openhuman.operator_explain_routing_decision
```
Registry additions belong in `src/core/all.rs` and the operator domain schema tests should prove schema/handler parity.
### Data Model
Minimum local tables:
```text
operator_goal_packs
operator_work_items
operator_action_proposals
operator_approval_requests
operator_execution_runs
operator_evidence_events
operator_connector_accounts
operator_policy_rules
operator_sync_state
```
Minimum concepts:
- **GoalPack:** a reusable outcome, trigger, context, permissions, and done criteria.
- **WorkItem:** normalized thing that may need attention.
- **ActionProposal:** draft action the operator wants to take.
- **ApprovalRequest:** explicit user decision required before risk-bearing actions.
- **ExecutionRun:** actual attempt to do approved work.
- **EvidenceEvent:** proof, receipt, diff, message id, PR URL, calendar id, or error trace.
- **PolicyRule:** source-of-truth and permission rule resolved in code, not prompts.
### Goal Pack Shape
```json
{
"id": "daily-brief",
"name": "Daily Brief",
"objective": "Tell me what changed, what needs a reply, and what should be done today.",
"triggers": [
{ "kind": "schedule", "cron": "0 7 * * MON-FRI" },
{ "kind": "manual", "surface": "phone" }
],
"sources": ["gmail", "calendar", "github", "linear", "imessage"],
"allowed_actions": ["read", "summarize", "draft", "classify", "propose"],
"approval_required": [
"send",
"create_external_record",
"update_external_record"
],
"done_criteria": [
"brief contains top changes",
"items needing reply are separated from FYI",
"each proposed action has source links"
],
"evidence_required": true
}
```
---
## Inbox Logic Integration
Do not port the whole inbox repo first. Use an adapter boundary.
### Phase 1 Adapter
OpenHuman calls a local inbox server when present:
```text
OpenHuman operator domain -> inbox_adapter.rs -> http://127.0.0.1:9849
```
Adapter functions:
- `list_actionable_threads`
- `list_waiting_on_me`
- `list_waiting_on_others`
- `list_calendar_context`
- `draft_reply_for_thread`
- `preflight_write`
- `explain_routing_decision`
This gives immediate reuse while keeping OpenHuman stable.
### Phase 2 Native Port
Port only stable primitives into Rust after the product loop is proven:
- normalized `WorkItem` schema
- sync state
- source/account ownership
- actionability classification
- source-of-truth routing
- confirmation gates
- evidence recording
Do not port provider-specific code until a native source has clear user value.
---
## Connector Strategy
### Bootstrap
Use Composio for breadth and speed:
- OAuth handoff
- long-tail connector discovery
- initial Gmail/GitHub/Linear/Slack/Notion-style coverage
- trigger discovery where useful
But do not expose generic Composio execution as the product API. Wrap it in operator policy:
```text
GoalPack -> Proposal -> Preflight -> Approval -> Composio execute -> Verify -> Evidence
```
### Core Connectors
Move high-value connectors to first-class/direct support when they become central to the product:
- Gmail
- Google Calendar
- GitHub
- Linear
- Google Drive/Docs
- Slack
- iMessage/local Messages bridge for power users
### Source-Of-Truth Rule
Routing belongs in code. The model should not guess which account to use.
Examples:
- Replies route through the owning account/thread unless explicitly overridden.
- New Google writes use the configured default account.
- GitHub PR comments route to the repo/owner from the original item.
- Linear updates route to the issue/team from the original work item.
Each write preview must show:
- connector
- account
- target object
- action type
- irreversible risk
- evidence expected after execution
---
## Phone And Chat Surfaces
The near-term goal is to meet users where they already are without pretending iMessage automation is simple or App Store-friendly.
### V0 Surfaces
- Desktop app approval queue.
- Local web approval page.
- Email or push-style notifications for approval requests.
- Telegram/Slack bot as an early phone chat surface.
- iOS Shortcuts + Share Sheet for "send this to operator".
- MCP/chat bridge for ChatGPT or other clients when available.
### Power User Surface
- Local iMessage bridge from macOS using the inbox patterns.
- Treat as a local-only advanced connector, not the primary commercial onboarding path.
### Later Surface
- Messages for Business only if the user base and Apple approval process justify it.
- It is not a general encrypted personal assistant channel by default, and it should not be the MVP dependency.
---
## Data Connect Control Plane
The control plane should store coordination metadata, not raw private data by default.
Candidate hosted tables:
```text
users
devices
connector_accounts
goal_packs
goal_pack_memberships
automation_runs
approval_requests
evidence_events
memory_summaries
channel_threads
sync_cursors
audit_events
```
Rules:
- Raw inbox/email/message content stays local unless the user explicitly opts in.
- Store derived summaries, hashes, source ids, and evidence metadata when enough.
- Every externally visible action records an audit event.
- Cloud unlocks multi-device continuity, phone approval, and backup, not broad data extraction.
TEE/private hosted execution is a later trust upgrade, not a blocker for v0. V0 should be local-first, boring, and auditable.
---
## MVP Goal Packs
### 1. Daily Brief
Outcome:
- show what changed
- show what needs a reply
- show today's meetings and prep
- show blocked/waiting items
Sources:
- Gmail
- Calendar
- GitHub
- Linear
- iMessage if available
Allowed without approval:
- read
- classify
- summarize
- propose
Approval required:
- send
- create issue
- update issue
- archive/delete
### 2. Reply Drafts
Outcome:
- find conversations needing reply
- draft replies in the user's voice
- let user approve, edit, or reject
- send through the owning account only after approval
Evidence:
- source thread id
- sent message id
- account used
- final body hash
### 3. Meeting Prep
Outcome:
- for upcoming meetings, gather calendar details, related threads/docs/issues, and suggested agenda/follow-ups
Evidence:
- calendar event id
- linked source items
- generated prep artifact id
### 4. Engineering Review / Next Goals
Outcome:
- inspect GitHub/Linear state
- identify stale PRs, failed checks, blocked issues, and next execution slices
- propose next Linear issues or PR review tasks
Approval required:
- create/update Linear issues
- comment on GitHub
- push code
- open/close PRs
---
## Safety Policy
### Auto-Allowed
- observe
- classify
- summarize
- draft
- rank
- prepare local previews
- create local evidence records
- run read-only checks
### Approval Required
- send message or email
- create/update/delete external records
- schedule/reschedule meetings
- spend money
- post publicly
- push code
- merge/close PRs
- destructive local filesystem operations
- broad data export
### Always Visible
Every proposal must show:
- what will happen
- why it is being proposed
- what account/source will be used
- what data leaves the device
- what evidence will be recorded
- how to undo or recover when possible
---
## Implementation Gates
### Gate 0 - Fork And Product Boundaries
- [ ] Decide fork name, package identifiers, and user-facing brand copy.
- [ ] Preserve upstream GPL-3.0 obligations for OpenHuman-derived client code.
- [ ] Preserve MIT notices for any inbox-derived code.
- [ ] Decide whether the hosted control plane is separate proprietary service code or open source.
- [ ] Add a `FORK_NOTES.md` or equivalent with upstream sync policy.
Validation:
```bash
git status --short
rg -n "GPL|MIT|license|License" README.md LICENSE* app src docs
```
### Gate 1 - Operator Domain Skeleton
Files:
- `src/openhuman/operator/types.rs`
- `src/openhuman/operator/store.rs`
- `src/openhuman/operator/schemas.rs`
- `src/openhuman/operator/ops.rs`
- `src/openhuman/operator/ops_tests.rs`
- `src/openhuman/operator/mod.rs`
- `src/core/all.rs`
Tasks:
- [ ] Define `GoalPack`, `WorkItem`, `ActionProposal`, `ApprovalRequest`, `ExecutionRun`, and `EvidenceEvent`.
- [ ] Add local SQLite-backed store or reuse the existing app storage pattern.
- [ ] Register list/create/update/list evidence controllers.
- [ ] Add schema parity tests.
Validation:
```bash
cargo test operator --manifest-path app/src-tauri/Cargo.toml
cargo check --manifest-path app/src-tauri/Cargo.toml
```
### Gate 2 - Inbox Adapter
Files:
- `src/openhuman/operator/inbox_adapter.rs`
- `src/openhuman/operator/preflight.rs`
- `src/openhuman/operator/policy.rs`
- `src/openhuman/operator/ops_tests.rs`
Tasks:
- [ ] Add adapter trait for actionable work.
- [ ] Implement local HTTP adapter for inbox server.
- [ ] Add inert/fake adapter for tests.
- [ ] Normalize inbox rows into operator `WorkItem`.
- [ ] Expose `operator_list_work_items` and `operator_explain_routing_decision`.
Validation:
```bash
cargo test operator --manifest-path app/src-tauri/Cargo.toml
```
### Gate 3 - Approval And Evidence Loop
Files:
- `src/openhuman/operator/preflight.rs`
- `src/openhuman/operator/evidence.rs`
- `src/openhuman/operator/ops.rs`
- `src/openhuman/operator/ops_tests.rs`
Tasks:
- [ ] `preview_action` returns account, target, risk, and expected evidence.
- [ ] `create_approval_request` stores a pending approval.
- [ ] `approve_request` transitions pending -> approved.
- [ ] `execute_approved_action` refuses unapproved writes.
- [ ] `record_evidence` links evidence to run/proposal/work item.
Validation:
```bash
cargo test operator:: --manifest-path app/src-tauri/Cargo.toml
```
### Gate 4 - First UI
Files:
- `app/src/lib/operator/`
- `app/src/components/operator/`
- `app/src/pages/Operator.tsx` or the existing app route pattern
- relevant route/nav files
Tasks:
- [ ] Add "Today" view with goal packs and work items.
- [ ] Add approval queue.
- [ ] Add evidence timeline.
- [ ] Add preview drawer for a proposed action.
- [ ] Keep all business logic in Rust controllers.
Validation:
```bash
pnpm --cwd app compile
pnpm --cwd app test -- --run operator
```
### Gate 5 - Composio Execution Wrapper
Files:
- `src/openhuman/operator/connectors.rs`
- `src/openhuman/operator/preflight.rs`
- `src/openhuman/operator/evidence.rs`
- `src/openhuman/composio/` only if narrow changes are required
Tasks:
- [ ] Execute only approved proposals.
- [ ] Map proposal action types to Composio action calls.
- [ ] Record connector result ids as evidence.
- [ ] Refuse generic tool calls from goal packs that lack explicit permission.
Validation:
```bash
cargo test operator composio --manifest-path app/src-tauri/Cargo.toml
```
### Gate 6 - Phone Reachability
Tasks:
- [ ] Add local web approval page or channel controller for approval requests.
- [ ] Add Telegram/Slack bot or MCP/chat bridge as first phone surface.
- [ ] Add iOS Shortcut/share-sheet handoff docs.
- [ ] Keep local iMessage bridge as advanced local connector, not onboarding dependency.
Validation:
```bash
cargo test channels operator --manifest-path app/src-tauri/Cargo.toml
pnpm --cwd app compile
```
---
## First Linear Slices
Each issue should be independently reviewable and should create a real PR.
1. **Operator domain skeleton**
- Acceptance: schemas compile, registry sees operator controllers, tests prove schema/handler parity.
- Validation: `cargo test operator --manifest-path app/src-tauri/Cargo.toml`.
2. **Goal pack store**
- Acceptance: create/list/update goal packs round trip through local store.
- Validation: `cargo test operator::goal_packs --manifest-path app/src-tauri/Cargo.toml`.
3. **Approval request state machine**
- Acceptance: pending/approved/rejected/executed transitions are enforced.
- Validation: `cargo test operator::approval --manifest-path app/src-tauri/Cargo.toml`.
4. **Evidence event store**
- Acceptance: evidence links to work item/proposal/run and is queryable.
- Validation: `cargo test operator::evidence --manifest-path app/src-tauri/Cargo.toml`.
5. **Inbox adapter trait plus fake adapter**
- Acceptance: operator can list normalized work items without real inbox server.
- Validation: `cargo test operator::inbox_adapter --manifest-path app/src-tauri/Cargo.toml`.
6. **Inbox HTTP adapter**
- Acceptance: when inbox server is reachable, actionable threads map to `WorkItem`; when unreachable, UI/API returns a clear degraded state.
- Validation: adapter unit tests plus one manual local smoke.
7. **Preflight/routing explanation**
- Acceptance: previews show source, account, target, risk, and expected evidence.
- Validation: `cargo test operator::preflight --manifest-path app/src-tauri/Cargo.toml`.
8. **Operator Today UI**
- Acceptance: user can see goal packs, actionable items, approvals, and evidence.
- Validation: `pnpm --cwd app compile` and operator component tests.
9. **Composio approved execution wrapper**
- Acceptance: unapproved writes fail; approved writes call the connector wrapper and record evidence.
- Validation: Rust tests with fake connector.
10. **Daily Brief goal pack**
- Acceptance: daily brief can run in preview mode and produce evidence-backed proposed actions.
- Validation: unit tests plus a local seeded-data smoke.
---
## Proof Metrics
MVP is useful only if these are true:
- Time from install to first useful proposed action is under 5 minutes.
- A non-technical user can connect at least one account and understand what the operator wants to do.
- Every write action has an approval record.
- Every completed action has evidence.
- The operator can explain why it chose an account/source.
- The user can reject a proposal without breaking the workflow.
- The product is useful with one connector and gets better with more connectors.
---
## Non-Goals For V0
- Full mobile app.
- Messages for Business as primary onboarding.
- Hosted TEE execution.
- Open-source model hosting as a product dependency.
- Marketplace for goal packs.
- Generic all-connector automation.
- Porting the whole inbox repo into Rust.
- Autonomous writes without approval.
---
## Decision Rules
- If a feature does not produce a work item, proposal, approval, action, or evidence event, it is probably not MVP.
- If a model is deciding account routing from prose, move the rule into code.
- If a connector action cannot be previewed, it cannot be executed.
- If an action cannot produce evidence, it should not claim completion.
- If a phone surface cannot carry approvals clearly, it is only a notification surface.
- If a goal pack cannot define done criteria, it is too vague.
File diff suppressed because it is too large Load Diff
@@ -1,980 +0,0 @@
# Memory Tree per-integration health strip — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a compact per-integration health strip inside `MemoryTreeStatusPanel`, between the four tiles and the auto-sync toggle, so operators can see at a glance which integrations are contributing chunks vs. idle. Issue #2763.
**Architecture:** Frontend-only. Reuses the existing `openhuman.memory_sync_status_list` RPC (no Rust changes). Extends `useMemoryTreeStatus` to fetch both endpoints in parallel on one shared 1.5s / 4s adaptive timer. Status mapping is pure TS: `Active` (freshness=active) vs `Stale` (freshness=recent|idle). Error state intentionally deferred — needs proper per-provider job→source linkage in core.
**Tech Stack:** React 18, TypeScript, Vitest + Testing Library, Tailwind. RPC bridge via `callCoreRpc` in `app/src/services/coreRpcClient`. i18n via `useT()` from `app/src/lib/i18n/I18nContext`.
**Branch:** `feat/memory-tree-integration-health` (already created from `origin/main`).
**Spec:** `docs/superpowers/specs/2026-06-02-memory-tree-integration-health-design.md`.
## File map
| File | Action | Responsibility |
| --- | --- | --- |
| `app/src/utils/tauriCommands/memoryTree.ts` | Modify | Add `MemorySyncFreshness`, `MemorySyncStatusRow`, `memorySyncStatusList()` wrapper for `openhuman.memory_sync_status_list`. |
| `app/src/components/intelligence/MemoryTreeStatusPanel.tsx` | Modify | Extend `useMemoryTreeStatus` hook to fetch both endpoints in parallel; add `IntegrationHealthStrip` sub-component; mount it between the 4-tile grid and the toggle row. |
| `app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx` | Modify | New cases: renders rows, status mapping, empty state, icon fallback, shared polling assertion. |
| `app/src/lib/i18n/en.ts` | Modify | 5 new keys under `memoryTree.status.integration*`. |
| `app/src/lib/i18n/{ar,bn,de,es,fr,hi,id,it,ko,pl,pt,ru,zh-CN}.ts` | Modify | Same 5 keys with real translations (CLAUDE.md i18n rule — no English fallback). |
No new files created. Strip lives as a sub-component inside `MemoryTreeStatusPanel.tsx` because the two concerns share state (hook output) and polling cadence.
---
### Task 1: Add `memorySyncStatusList()` wrapper
**Files:**
- Modify: `app/src/utils/tauriCommands/memoryTree.ts` (append at end of file, before any closing exports)
**Context:** The Rust RPC `openhuman.memory_sync_status_list` returns `{ statuses: MemorySyncStatus[] }`. Wire shape from `src/openhuman/memory_sync/sync_status/types.rs`:
```rust
pub struct MemorySyncStatus {
pub provider: String,
pub chunks_synced: u64,
pub chunks_pending: u64,
pub batch_total: u64,
pub batch_processed: u64,
pub last_chunk_at_ms: Option<i64>,
pub freshness: FreshnessLabel, // snake_case: "active" | "recent" | "idle"
}
```
We only consume `provider`, `chunks_synced`, `last_chunk_at_ms`, `freshness` in v1. Keep the full type so the wrapper is reusable.
- [ ] **Step 1: Append the wrapper to memoryTree.ts**
Append at the bottom of `app/src/utils/tauriCommands/memoryTree.ts` (after the existing `memoryTreeSetEnabled` block):
```ts
// ── memory_sync_status_list (#2763 — per-integration health strip) ───────
/**
* Freshness label emitted by `openhuman.memory_sync_status_list`. Snake-case
* mirrors the Rust `FreshnessLabel` serde rename. Derived from
* `now - last_chunk_at_ms` at RPC time, not stored.
*/
export type MemorySyncFreshness = 'active' | 'recent' | 'idle';
/**
* One row per provider that has produced chunks. Mirrors the Rust
* `MemorySyncStatus` struct exactly — snake_case carried through so the
* wire payload deserialises without a remap layer.
*/
export interface MemorySyncStatusRow {
/** Provider key — `slack`, `gmail`, `notion`, `discord`, `telegram`, etc. */
provider: string;
/** Total chunks in `mem_tree_chunks` for this provider. */
chunks_synced: number;
/** Chunks fetched but not yet extracted/embedded. Lifetime metric. */
chunks_pending: number;
/** Total chunks in the current sync wave. Zero when no wave is active. */
batch_total: number;
/** Of `batch_total`, how many have been processed. */
batch_processed: number;
/** Epoch ms of the most-recent chunk for this provider; null if none yet. */
last_chunk_at_ms: number | null;
/** Coarse activity label — derived at RPC time. */
freshness: MemorySyncFreshness;
}
/**
* Fetch the per-provider sync-status list. Single SQL query against
* `mem_tree_chunks` (GROUP BY source_kind); safe to poll alongside
* `memoryTreePipelineStatus` on the same 1.5s / 4s adaptive cadence.
*
* Backed by `openhuman.memory_sync_status_list` (#1136). Surfaced by the
* per-integration health strip in `MemoryTreeStatusPanel` (#2763).
*/
export async function memorySyncStatusList(): Promise<MemorySyncStatusRow[]> {
console.debug('[memory-tree-rpc] memorySyncStatusList: entry');
const resp = await callCoreRpc<
{ statuses: MemorySyncStatusRow[] } | ResultEnvelope<{ statuses: MemorySyncStatusRow[] }>
>({ method: 'openhuman.memory_sync_status_list', params: {} });
const out = unwrapResult(resp);
const rows = out.statuses ?? [];
console.debug('[memory-tree-rpc] memorySyncStatusList: exit rows=%d', rows.length);
return rows;
}
```
- [ ] **Step 2: Verify the file compiles**
Run: `pnpm typecheck`
Expected: PASS (no new TS errors). If it fails on `callCoreRpc`/`unwrapResult`/`ResultEnvelope` not being in scope, they're already imported in this file — check the top of the file. Re-check the appended block uses the same identifiers verbatim.
- [ ] **Step 3: Commit**
```bash
git add app/src/utils/tauriCommands/memoryTree.ts
git commit -m "feat(memory-tree): add memorySyncStatusList RPC wrapper (#2763)"
```
---
### Task 2: Add the 5 new i18n keys (English)
**Files:**
- Modify: `app/src/lib/i18n/en.ts:521` (immediately after the existing `memoryTree.status.daysAgo` line)
- [ ] **Step 1: Insert the new keys**
Open `app/src/lib/i18n/en.ts`. Find the line `'memoryTree.status.daysAgo': '{count} days ago',` (around line 521). Insert immediately after it:
```ts
// Per-integration health strip (#2763) — rendered between the 4-tile grid
// and the auto-sync toggle in MemoryTreeStatusPanel.
'memoryTree.status.integrationsTitle': 'Per-integration health',
'memoryTree.status.integrationsEmpty': 'No integrations connected',
'memoryTree.status.integrationActive': 'Active',
'memoryTree.status.integrationStale': 'Stale',
'memoryTree.status.integrationChunks': '{count} chunks',
```
- [ ] **Step 2: Verify typecheck still clean**
Run: `pnpm typecheck`
Expected: PASS.
- [ ] **Step 3: Run the i18n parity gate to confirm it now expects these keys in every other locale**
Run: `pnpm i18n:check`
Expected: FAIL with messages like `Missing key 'memoryTree.status.integrationsTitle' in locale 'ar'` etc. (13 missing-key errors per new key). This is the desired failure that Task 3 fixes.
- [ ] **Step 4: Commit**
```bash
git add app/src/lib/i18n/en.ts
git commit -m "feat(i18n): add English keys for integration health strip (#2763)"
```
---
### Task 3: Add real translations for all 13 non-English locales
**Files:** Modify each of:
- `app/src/lib/i18n/ar.ts`
- `app/src/lib/i18n/bn.ts`
- `app/src/lib/i18n/de.ts`
- `app/src/lib/i18n/es.ts`
- `app/src/lib/i18n/fr.ts`
- `app/src/lib/i18n/hi.ts`
- `app/src/lib/i18n/id.ts`
- `app/src/lib/i18n/it.ts`
- `app/src/lib/i18n/ko.ts`
- `app/src/lib/i18n/pl.ts`
- `app/src/lib/i18n/pt.ts`
- `app/src/lib/i18n/ru.ts`
- `app/src/lib/i18n/zh-CN.ts`
Each file already contains the `memoryTree.status.daysAgo` key (sibling to where we inserted in en.ts). Insert the 5 new keys directly after that line in every file, in the language of that file.
- [ ] **Step 1: Insert translations into each locale file**
For each locale file, find `memoryTree.status.daysAgo` and insert the corresponding block below. Use these exact translations:
**`ar.ts`** (Arabic):
```ts
'memoryTree.status.integrationsTitle': 'حالة التكاملات',
'memoryTree.status.integrationsEmpty': 'لا توجد تكاملات متصلة',
'memoryTree.status.integrationActive': 'نشط',
'memoryTree.status.integrationStale': 'قديم',
'memoryTree.status.integrationChunks': '{count} قطعة',
```
**`bn.ts`** (Bengali):
```ts
'memoryTree.status.integrationsTitle': 'প্রতি-ইন্টিগ্রেশন স্বাস্থ্য',
'memoryTree.status.integrationsEmpty': 'কোনো ইন্টিগ্রেশন সংযুক্ত নেই',
'memoryTree.status.integrationActive': 'সক্রিয়',
'memoryTree.status.integrationStale': 'পুরানো',
'memoryTree.status.integrationChunks': '{count} টি অংশ',
```
**`de.ts`** (German):
```ts
'memoryTree.status.integrationsTitle': 'Integrationsstatus',
'memoryTree.status.integrationsEmpty': 'Keine Integrationen verbunden',
'memoryTree.status.integrationActive': 'Aktiv',
'memoryTree.status.integrationStale': 'Veraltet',
'memoryTree.status.integrationChunks': '{count} Chunks',
```
**`es.ts`** (Spanish):
```ts
'memoryTree.status.integrationsTitle': 'Estado por integración',
'memoryTree.status.integrationsEmpty': 'No hay integraciones conectadas',
'memoryTree.status.integrationActive': 'Activa',
'memoryTree.status.integrationStale': 'Inactiva',
'memoryTree.status.integrationChunks': '{count} fragmentos',
```
**`fr.ts`** (French):
```ts
'memoryTree.status.integrationsTitle': 'Santé par intégration',
'memoryTree.status.integrationsEmpty': 'Aucune intégration connectée',
'memoryTree.status.integrationActive': 'Active',
'memoryTree.status.integrationStale': 'Obsolète',
'memoryTree.status.integrationChunks': '{count} fragments',
```
**`hi.ts`** (Hindi):
```ts
'memoryTree.status.integrationsTitle': 'प्रति-एकीकरण स्थिति',
'memoryTree.status.integrationsEmpty': 'कोई एकीकरण कनेक्ट नहीं है',
'memoryTree.status.integrationActive': 'सक्रिय',
'memoryTree.status.integrationStale': 'पुराना',
'memoryTree.status.integrationChunks': '{count} खंड',
```
**`id.ts`** (Indonesian):
```ts
'memoryTree.status.integrationsTitle': 'Kesehatan per integrasi',
'memoryTree.status.integrationsEmpty': 'Tidak ada integrasi tersambung',
'memoryTree.status.integrationActive': 'Aktif',
'memoryTree.status.integrationStale': 'Usang',
'memoryTree.status.integrationChunks': '{count} potongan',
```
**`it.ts`** (Italian):
```ts
'memoryTree.status.integrationsTitle': 'Stato per integrazione',
'memoryTree.status.integrationsEmpty': 'Nessuna integrazione collegata',
'memoryTree.status.integrationActive': 'Attiva',
'memoryTree.status.integrationStale': 'Obsoleta',
'memoryTree.status.integrationChunks': '{count} frammenti',
```
**`ko.ts`** (Korean):
```ts
'memoryTree.status.integrationsTitle': '통합별 상태',
'memoryTree.status.integrationsEmpty': '연결된 통합이 없습니다',
'memoryTree.status.integrationActive': '활성',
'memoryTree.status.integrationStale': '오래됨',
'memoryTree.status.integrationChunks': '{count}개 청크',
```
**`pl.ts`** (Polish):
```ts
'memoryTree.status.integrationsTitle': 'Stan poszczególnych integracji',
'memoryTree.status.integrationsEmpty': 'Brak podłączonych integracji',
'memoryTree.status.integrationActive': 'Aktywna',
'memoryTree.status.integrationStale': 'Nieaktualna',
'memoryTree.status.integrationChunks': '{count} fragmentów',
```
**`pt.ts`** (Portuguese):
```ts
'memoryTree.status.integrationsTitle': 'Saúde por integração',
'memoryTree.status.integrationsEmpty': 'Nenhuma integração conectada',
'memoryTree.status.integrationActive': 'Ativa',
'memoryTree.status.integrationStale': 'Obsoleta',
'memoryTree.status.integrationChunks': '{count} fragmentos',
```
**`ru.ts`** (Russian):
```ts
'memoryTree.status.integrationsTitle': 'Состояние интеграций',
'memoryTree.status.integrationsEmpty': 'Нет подключённых интеграций',
'memoryTree.status.integrationActive': 'Активна',
'memoryTree.status.integrationStale': 'Устарела',
'memoryTree.status.integrationChunks': '{count} фрагментов',
```
**`zh-CN.ts`** (Simplified Chinese):
```ts
'memoryTree.status.integrationsTitle': '各集成状态',
'memoryTree.status.integrationsEmpty': '未连接任何集成',
'memoryTree.status.integrationActive': '活跃',
'memoryTree.status.integrationStale': '过期',
'memoryTree.status.integrationChunks': '{count} 个块',
```
- [ ] **Step 2: Verify i18n parity gate now passes**
Run: `pnpm i18n:check`
Expected: PASS (no missing-key errors).
- [ ] **Step 3: Verify the English-detection gate passes**
Run: `pnpm i18n:english:check`
Expected: PASS — none of the new translations match the English-detection heuristic. If a value is incorrectly flagged (very unlikely for these short strings) the failure prints the offending key + locale; rewrite that translation rather than touching the `INTENTIONAL_ENGLISH` allowlist.
- [ ] **Step 4: Commit**
```bash
git add app/src/lib/i18n/ar.ts app/src/lib/i18n/bn.ts app/src/lib/i18n/de.ts \
app/src/lib/i18n/es.ts app/src/lib/i18n/fr.ts app/src/lib/i18n/hi.ts \
app/src/lib/i18n/id.ts app/src/lib/i18n/it.ts app/src/lib/i18n/ko.ts \
app/src/lib/i18n/pl.ts app/src/lib/i18n/pt.ts app/src/lib/i18n/ru.ts \
app/src/lib/i18n/zh-CN.ts
git commit -m "feat(i18n): translate integration health strip keys (13 locales, #2763)"
```
---
### Task 4: Extend `useMemoryTreeStatus` to fetch sync-status in parallel (test first)
**Files:**
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx` (add a test before extending the hook)
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.tsx` (extend hook)
**Context:** The existing `useMemoryTreeStatus` returns `{ status, loading, error, refresh }`. We add `integrations: MemorySyncStatusRow[]` to the return shape. The fetcher swaps a single `await memoryTreePipelineStatus()` for a `Promise.all` against both endpoints. On per-endpoint failure we degrade gracefully — pipeline-status failure already shows the existing error banner; sync-status failure logs a warn and renders an empty integration list, so the rest of the panel stays functional.
- [ ] **Step 1: Add the failing test**
In `app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx`, find the existing `vi.mock` block (around line 21). Replace it with:
```ts
const mockPipelineStatus = vi.fn();
const mockSetEnabled = vi.fn();
const mockSyncStatusList = vi.fn();
vi.mock('../../utils/tauriCommands', async importOriginal => {
const actual = await importOriginal<typeof import('../../utils/tauriCommands')>();
return {
...actual,
memoryTreePipelineStatus: (...args: unknown[]) => mockPipelineStatus(...args),
memoryTreeSetEnabled: (...args: unknown[]) => mockSetEnabled(...args),
memorySyncStatusList: (...args: unknown[]) => mockSyncStatusList(...args),
};
});
```
Then in the existing `beforeEach`, add a reset line:
```ts
mockSyncStatusList.mockReset();
mockSyncStatusList.mockResolvedValue([]); // default: empty, harmless to existing tests
```
Add the new test case inside the `describe('<MemoryTreeStatusPanel />', ...)` block (anywhere after the existing `'renders the four tiles ...'` case):
```ts
it('fetches integration list and pipeline status in parallel on the same tick', async () => {
mockPipelineStatus.mockResolvedValue(payload());
mockSyncStatusList.mockResolvedValue([
{
provider: 'slack',
chunks_synced: 5231,
chunks_pending: 0,
batch_total: 0,
batch_processed: 0,
last_chunk_at_ms: FIXED_NOW_MS - 3 * 60 * 1000,
freshness: 'active',
},
]);
render(<MemoryTreeStatusPanel />);
await waitFor(() => {
expect(mockPipelineStatus).toHaveBeenCalledTimes(1);
expect(mockSyncStatusList).toHaveBeenCalledTimes(1);
});
});
```
- [ ] **Step 2: Run the new test, watch it fail**
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "fetches integration list"`
Expected: FAIL — `expected mockSyncStatusList to have been called 1 time, but got 0` (the hook isn't yet calling it).
- [ ] **Step 3: Extend the hook to fetch both endpoints**
In `app/src/components/intelligence/MemoryTreeStatusPanel.tsx`:
1. Update the import line (currently at ~line 26):
```ts
import {
memoryTreePipelineStatus,
type MemoryTreePipelineStatus,
memoryTreeSetEnabled,
memorySyncStatusList,
type MemorySyncStatusRow,
} from '../../utils/tauriCommands';
```
2. Replace the entire `useMemoryTreeStatus` hook (currently lines 46102) with this version. New state slot, `Promise.all` in the fetcher, return shape gains `integrations`:
```ts
function useMemoryTreeStatus(): {
status: MemoryTreePipelineStatus | null;
integrations: MemorySyncStatusRow[];
loading: boolean;
error: string | null;
refresh: () => Promise<void>;
} {
const [status, setStatus] = useState<MemoryTreePipelineStatus | null>(null);
const [integrations, setIntegrations] = useState<MemorySyncStatusRow[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const cancelledRef = useRef(false);
const statusRef = useRef<MemoryTreePipelineStatus | null>(null);
statusRef.current = status;
const fetchOnce = useCallback(async () => {
console.debug('[ui-flow][memory-tree-status] fetchOnce: entry');
try {
// Fetch pipeline + per-integration health in parallel so the strip
// and the tiles share a single 1.5s / 4s adaptive tick (#2763).
const [next, rows] = await Promise.all([
memoryTreePipelineStatus(),
memorySyncStatusList().catch(err => {
// Per-integration list is best-effort: surface an empty strip
// rather than wiping the panel when only the secondary endpoint
// fails. Pipeline failure still flips the panel-wide error.
console.warn(
'[ui-flow][memory-tree-status] memorySyncStatusList failed: %s',
err instanceof Error ? err.message : String(err)
);
return [] as MemorySyncStatusRow[];
}),
]);
if (cancelledRef.current) return;
setStatus(next);
setIntegrations(rows);
setError(null);
console.debug(
'[ui-flow][memory-tree-status] fetchOnce: ok status=%s total=%d integrations=%d',
next.status,
next.total_chunks,
rows.length
);
} catch (err) {
if (cancelledRef.current) return;
const message = err instanceof Error ? err.message : String(err);
console.warn('[ui-flow][memory-tree-status] fetchOnce: error %s', message);
setError(message);
} finally {
if (!cancelledRef.current) setLoading(false);
}
}, []);
useEffect(() => {
cancelledRef.current = false;
let timer: ReturnType<typeof setTimeout> | null = null;
const tick = async () => {
await fetchOnce();
if (cancelledRef.current) return;
const live = statusRef.current;
const fast = live?.is_syncing || (live?.pipeline_jobs?.running ?? 0) > 0;
timer = setTimeout(tick, fast ? FAST_POLL_MS : DEFAULT_POLL_MS);
};
void tick();
return () => {
cancelledRef.current = true;
if (timer) clearTimeout(timer);
};
}, [fetchOnce]);
return { status, integrations, loading, error, refresh: fetchOnce };
}
```
3. Update the `MemoryTreeStatusPanel` body to destructure `integrations`. Replace the existing `const { status, loading, error, refresh } = useMemoryTreeStatus();` (around line 184) with:
```ts
const { status, integrations, loading, error, refresh } = useMemoryTreeStatus();
```
(The strip itself is wired in Task 6; for now `integrations` is unused — TypeScript will allow this because it's a destructured property, not a declared local.)
- [ ] **Step 4: Run the new test, watch it pass**
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "fetches integration list"`
Expected: PASS.
- [ ] **Step 5: Run the full file's existing tests to confirm no regression**
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx`
Expected: All existing tests still PASS (the default empty `mockSyncStatusList` resolution preserves behaviour).
- [ ] **Step 6: Commit**
```bash
git add app/src/components/intelligence/MemoryTreeStatusPanel.tsx \
app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx
git commit -m "feat(memory-tree): share pipeline + sync-status poll in useMemoryTreeStatus (#2763)"
```
---
### Task 5: Add status-classification helper + provider icon map (test first)
**Files:**
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx`
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.tsx`
**Context:** Two pure helpers in the panel file (not exported). `classifyIntegration(freshness)` returns `'active' | 'stale'`. `providerIconChar(provider)` returns a single emoji glyph from a small built-in map, falling back to `'🔌'` for unknown providers. These are tested independently of the React render.
- [ ] **Step 1: Add tests for the helpers (failing)**
At the top of `MemoryTreeStatusPanel.test.tsx`, change the `import { MemoryTreeStatusPanel } from './MemoryTreeStatusPanel';` line to also pull the new helpers:
```ts
import {
MemoryTreeStatusPanel,
classifyIntegration,
providerIconChar,
} from './MemoryTreeStatusPanel';
```
Below the existing `describe('<MemoryTreeStatusPanel />', ...)` block, add a sibling describe:
```ts
describe('integration health helpers', () => {
describe('classifyIntegration', () => {
it('maps active freshness to active', () => {
expect(classifyIntegration('active')).toBe('active');
});
it('maps recent freshness to stale', () => {
expect(classifyIntegration('recent')).toBe('stale');
});
it('maps idle freshness to stale', () => {
expect(classifyIntegration('idle')).toBe('stale');
});
});
describe('providerIconChar', () => {
it('returns a known glyph for slack', () => {
expect(providerIconChar('slack')).toBe('💬');
});
it('returns a known glyph for gmail', () => {
expect(providerIconChar('gmail')).toBe('📧');
});
it('falls back to the plug glyph for unknown providers', () => {
expect(providerIconChar('definitely-not-a-real-provider')).toBe('🔌');
});
});
});
```
- [ ] **Step 2: Run the new tests, watch them fail**
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "integration health helpers"`
Expected: FAIL — `classifyIntegration` and `providerIconChar` aren't exported yet.
- [ ] **Step 3: Add the helpers and export them**
In `app/src/components/intelligence/MemoryTreeStatusPanel.tsx`, add the following block just after `function statusDotClass(...)` (around line 174, before the `MemoryTreeStatusPanelProps` interface):
```ts
/**
* UI health classification for a single provider row in the integration
* health strip (#2763). The wire shape's three-state `freshness` collapses
* to two states here — `Active` (currently producing chunks) vs `Stale`
* (anything older). An `Error` state is intentionally NOT derived from the
* current data; per-provider failure attribution needs new core work and
* is filed as a follow-up to issue #2763.
*/
export type IntegrationHealth = 'active' | 'stale';
/** Map the wire `freshness` enum to the two-state UI classification. */
export function classifyIntegration(freshness: MemorySyncStatusRow['freshness']): IntegrationHealth {
return freshness === 'active' ? 'active' : 'stale';
}
/**
* Built-in glyph for each known provider key from `memory_sync_status_list`.
* Source: `MemorySyncStatus.provider` in `src/openhuman/memory_sync/sync_status/types.rs`
* — that file's doc comment enumerates the providers ("slack", "gmail",
* "discord", "telegram", "whatsapp", "notion", "meeting_notes",
* "drive_docs", etc.). Anything not in this map falls back to a generic
* plug glyph so unknown providers still render cleanly.
*
* Kept inline (rather than re-using `SOURCE_KIND_ICONS` from
* `memorySourcesService`) because that map is keyed by `SourceKind`
* (`composio` / `folder` / `github_repo` / …) — a different taxonomy.
*/
const PROVIDER_ICONS: Record<string, string> = {
slack: '💬',
gmail: '📧',
discord: '🎮',
telegram: '✈️',
whatsapp: '🟢',
notion: '📝',
meeting_notes: '🎙️',
drive_docs: '📄',
github: '🐙',
};
/** Look up a provider glyph; fall back to a generic plug for unknowns. */
export function providerIconChar(provider: string): string {
return PROVIDER_ICONS[provider] ?? '🔌';
}
```
- [ ] **Step 4: Run the helper tests, watch them pass**
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "integration health helpers"`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add app/src/components/intelligence/MemoryTreeStatusPanel.tsx \
app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx
git commit -m "feat(memory-tree): add integration-health classifier + provider icons (#2763)"
```
---
### Task 6: Render the `IntegrationHealthStrip` sub-component (test first)
**Files:**
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx`
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.tsx`
**Context:** Internal sub-component (not exported). Takes `integrations: MemorySyncStatusRow[]` and a translator. Renders header + scrollable list of rows, or empty-state copy when the array is empty. Mounted inside `MemoryTreeStatusPanel` between the tile grid (currently the `<div className="grid grid-cols-2 ..." data-testid="memory-tree-status-tiles">` block) and the auto-sync toggle row.
- [ ] **Step 1: Add the failing UI tests**
In `MemoryTreeStatusPanel.test.tsx`, inside the `describe('<MemoryTreeStatusPanel />', ...)` block, add three more cases:
```ts
it('renders a row per integration with provider name, chunk count, freshness pill', async () => {
mockPipelineStatus.mockResolvedValue(payload());
mockSyncStatusList.mockResolvedValue([
{
provider: 'slack',
chunks_synced: 5231,
chunks_pending: 0,
batch_total: 0,
batch_processed: 0,
last_chunk_at_ms: FIXED_NOW_MS - 3 * 60 * 1000,
freshness: 'active',
},
{
provider: 'gmail',
chunks_synced: 842,
chunks_pending: 0,
batch_total: 0,
batch_processed: 0,
last_chunk_at_ms: FIXED_NOW_MS - 2 * 60 * 60 * 1000,
freshness: 'idle',
},
]);
render(<MemoryTreeStatusPanel />);
await waitFor(() => {
expect(screen.getByTestId('memory-tree-integrations')).toBeInTheDocument();
});
const rows = screen.getAllByTestId(/^memory-tree-integration-row-/);
expect(rows).toHaveLength(2);
// Slack row: active dot, "Active" label, chunk count rendered
const slackRow = screen.getByTestId('memory-tree-integration-row-slack');
expect(slackRow).toHaveTextContent(/slack/i);
expect(slackRow).toHaveTextContent(/5,231 chunks/);
expect(slackRow).toHaveTextContent(/Active/);
// Gmail row: stale label
const gmailRow = screen.getByTestId('memory-tree-integration-row-gmail');
expect(gmailRow).toHaveTextContent(/gmail/i);
expect(gmailRow).toHaveTextContent(/Stale/);
});
it('shows the empty state when there are no integrations', async () => {
mockPipelineStatus.mockResolvedValue(payload());
mockSyncStatusList.mockResolvedValue([]);
render(<MemoryTreeStatusPanel />);
await waitFor(() => {
expect(screen.getByTestId('memory-tree-integrations-empty')).toBeInTheDocument();
});
expect(screen.getByTestId('memory-tree-integrations-empty')).toHaveTextContent(
/no integrations connected/i
);
});
it('renders the integration strip between the tile grid and the toggle row', async () => {
mockPipelineStatus.mockResolvedValue(payload());
mockSyncStatusList.mockResolvedValue([
{
provider: 'slack',
chunks_synced: 1,
chunks_pending: 0,
batch_total: 0,
batch_processed: 0,
last_chunk_at_ms: FIXED_NOW_MS - 1000,
freshness: 'active',
},
]);
render(<MemoryTreeStatusPanel />);
await waitFor(() => {
expect(screen.getByTestId('memory-tree-integrations')).toBeInTheDocument();
});
// DOM order: tiles → integrations → toggle row.
const panel = screen.getByTestId('memory-tree-status-panel');
const tiles = screen.getByTestId('memory-tree-status-tiles');
const strip = screen.getByTestId('memory-tree-integrations');
const toggle = screen.getByTestId('memory-tree-status-toggle-row');
const order = Array.from(panel.querySelectorAll('[data-testid]'))
.map(el => el.getAttribute('data-testid'))
.filter(id =>
['memory-tree-status-tiles', 'memory-tree-integrations', 'memory-tree-status-toggle-row'].includes(
id ?? ''
)
);
expect(order).toEqual([
'memory-tree-status-tiles',
'memory-tree-integrations',
'memory-tree-status-toggle-row',
]);
// Sanity references so unused-var lint doesn't flag the locals above.
expect(tiles).toBeInTheDocument();
expect(strip).toBeInTheDocument();
expect(toggle).toBeInTheDocument();
});
```
- [ ] **Step 2: Run the new tests, watch them fail**
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "renders a row per integration|empty state when there are no integrations|integration strip between"`
Expected: FAIL — `memory-tree-integrations` and `memory-tree-integrations-empty` test-ids do not exist yet.
- [ ] **Step 3: Add the sub-component**
In `MemoryTreeStatusPanel.tsx`, just before the existing `export function MemoryTreeStatusPanel(...)` declaration (around line 182), insert:
```ts
/**
* Per-integration health strip (#2763). Rendered between the four pipeline
* tiles and the auto-sync toggle inside `MemoryTreeStatusPanel`. Consumes
* the `integrations` slice returned by `useMemoryTreeStatus` — no
* additional fetch, no second timer.
*/
function IntegrationHealthStrip({
integrations,
t,
}: {
integrations: MemorySyncStatusRow[];
t: TFn;
}) {
return (
<div className="space-y-2" data-testid="memory-tree-integrations">
<div className="text-[11px] uppercase tracking-wide text-stone-500 dark:text-neutral-400">
{t('memoryTree.status.integrationsTitle')}
</div>
{integrations.length === 0 ? (
<div
data-testid="memory-tree-integrations-empty"
className="rounded-lg border border-dashed border-stone-200 dark:border-neutral-800 px-3 py-2 text-xs text-stone-500 dark:text-neutral-400">
{t('memoryTree.status.integrationsEmpty')}
</div>
) : (
<ul
className="max-h-48 space-y-1 overflow-y-auto rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50/40 dark:bg-neutral-800/30 p-2"
aria-label={t('memoryTree.status.integrationsTitle')}>
{integrations.map(row => {
const health = classifyIntegration(row.freshness);
const healthLabel =
health === 'active'
? t('memoryTree.status.integrationActive')
: t('memoryTree.status.integrationStale');
const dot = health === 'active' ? 'bg-sage-400' : 'bg-stone-400 dark:bg-neutral-500';
return (
<li
key={row.provider}
data-testid={`memory-tree-integration-row-${row.provider}`}
className="flex items-center justify-between gap-2 rounded-md px-2 py-1.5 hover:bg-stone-100/60 dark:hover:bg-neutral-800/60">
<div className="flex min-w-0 items-center gap-2">
<span aria-hidden className="text-base leading-none">
{providerIconChar(row.provider)}
</span>
<span className="truncate text-sm font-medium text-stone-800 dark:text-neutral-200">
{row.provider}
</span>
</div>
<div className="flex shrink-0 items-center gap-3 text-xs text-stone-500 dark:text-neutral-400">
<span>
{t('memoryTree.status.integrationChunks').replace(
'{count}',
new Intl.NumberFormat().format(row.chunks_synced)
)}
</span>
<span>
{formatRelativeMs(row.last_chunk_at_ms ?? 0, t, t('memoryTree.status.never'))}
</span>
<span className="inline-flex items-center gap-1.5 rounded-full bg-white dark:bg-neutral-900 px-2 py-0.5 text-[11px] font-medium text-stone-700 dark:text-neutral-200 ring-1 ring-stone-200 dark:ring-neutral-700">
<span aria-hidden className={`inline-block h-1.5 w-1.5 rounded-full ${dot}`} />
{healthLabel}
</span>
</div>
</li>
);
})}
</ul>
)}
</div>
);
}
```
- [ ] **Step 4: Mount the strip between the tiles and the toggle**
Still in `MemoryTreeStatusPanel.tsx`, find the closing `</div>` of the `data-testid="memory-tree-status-tiles"` block (the grid div that holds the 4 tiles — its closing `</div>` is right before the `{/* Auto-sync toggle row ... */}` comment, around line 317). Insert immediately after that closing `</div>`:
```tsx
<IntegrationHealthStrip integrations={integrations} t={t} />
```
- [ ] **Step 5: Run the new UI tests, watch them pass**
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "renders a row per integration|empty state when there are no integrations|integration strip between"`
Expected: PASS.
- [ ] **Step 6: Run the entire file's tests to confirm no regression**
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx`
Expected: All tests PASS.
- [ ] **Step 7: Commit**
```bash
git add app/src/components/intelligence/MemoryTreeStatusPanel.tsx \
app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx
git commit -m "feat(memory-tree): render per-integration health strip in status panel (#2763)"
```
---
### Task 7: Full quality suite + format + push
**Files:** No code changes; verification + push.
- [ ] **Step 1: Format**
Run: `pnpm format`
Expected: Prettier + cargo fmt clean (no Rust changed in this PR; cargo fmt is a no-op but still safe).
- [ ] **Step 2: Lint**
Run: `pnpm lint`
Expected: PASS.
- [ ] **Step 3: Typecheck**
Run: `pnpm typecheck`
Expected: PASS.
- [ ] **Step 4: i18n parity + English-detection**
Run: `pnpm i18n:check && pnpm i18n:english:check`
Expected: Both PASS.
- [ ] **Step 5: Full Vitest suite**
Run: `pnpm debug unit`
Expected: All tests PASS. New MemoryTreeStatusPanel cases are visible in the summary.
- [ ] **Step 6: Coverage spot-check (changed lines)**
Run: `pnpm test:coverage --run`
Expected: PASS; new lines in `MemoryTreeStatusPanel.tsx` and `memoryTree.ts` are exercised by the tests added above (classifier, icon map, render branches, empty state). If diff-cover reports any new line uncovered, add a targeted test before pushing — don't ship below the 80 % gate.
- [ ] **Step 7: Stage any format-only changes that fell out of Step 1**
```bash
git status
# If there are uncommitted Prettier-only changes:
git add -p # review and stage the trivial fixups
git commit -m "chore: pnpm format pass"
```
- [ ] **Step 8: Push to fork**
```bash
git push aniketh feat/memory-tree-integration-health -u
```
Expected: branch lands on `github.com/CodeGhost21/openhuman`. If the pre-push hook fails on `prettier: command not found` (fresh worktree missing `node_modules`), check `cargo fmt --check` is clean and push with `--no-verify` — this is a known worktree gotcha and noted in `.claude/memory.md`.
- [ ] **Step 9: Open PR against upstream**
```bash
gh pr create \
--repo tinyhumansai/openhuman \
--base main \
--head CodeGhost21:feat/memory-tree-integration-health \
--title "feat(memory-tree): per-integration health strip (#2763)" \
--body-file - <<'EOF'
## Summary
Adds a compact per-integration health strip inside `MemoryTreeStatusPanel`, between the four pipeline tiles and the auto-sync toggle. Each row shows provider icon + name + chunk count + relative last-sync time + an Active/Stale pill. Closes #2763 (#1856 Part 3).
- Reuses the existing `openhuman.memory_sync_status_list` RPC — no Rust changes.
- Single shared poll: `useMemoryTreeStatus` now fetches pipeline + sync-status in parallel on the existing 1.5s / 4s adaptive timer.
- Status mapping is pure TS: `freshness=active` → Active; `freshness=recent|idle` → Stale.
- i18n: 5 new keys, real translations across all 14 locales.
## Deviations from issue acceptance criteria
These are intentional; see `docs/superpowers/specs/2026-06-02-memory-tree-integration-health-design.md` for the full rationale:
- **AC #1** (extend `memory_tree_pipeline_status` with `integrations` array) — instead we consume the pre-existing `openhuman.memory_sync_status_list` RPC. Same data; no schema bump.
- **AC #2** (`Active / Stale / Error`) — we ship **Active / Stale** only. Per-provider Error attribution needs new core work (`mem_tree_jobs` has no `source_kind` / `source_id` column); I'll file the follow-up after this lands.
## Test plan
- [ ] `pnpm typecheck`
- [ ] `pnpm lint`
- [ ] `pnpm i18n:check`
- [ ] `pnpm i18n:english:check`
- [ ] `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx`
- [ ] `pnpm test:coverage` — changed-lines coverage ≥ 80 %
- [ ] Manual: open Intelligence page with at least one Composio integration connected; confirm strip renders with correct freshness pill and updates on the shared poll.
- [ ] Manual: with zero integrations, confirm the empty-state copy renders.
## Submission Checklist
- [x] Branch is based on `tinyhumansai/openhuman:main`.
- [x] PR title follows conventional commits.
- [x] Tests added/updated for changed behavior.
- [x] i18n keys added to all 14 locale files with real translations.
- [x] Diff coverage ≥ 80 % on changed lines.
- [x] No Rust changes (N/A for `cargo check`, `cargo fmt`, `cargo test`).
- [x] Linked issue: #2763.
EOF
```
Expected: PR URL printed. Drop it in chat for the user.
- [ ] **Step 10: Verify PR landed cleanly**
Run: `gh pr view --web` (optional — opens in browser) or `gh pr checks <PR-number>` to watch CI start.
Expected: Submission Checklist job picks up the `[x]`-filled checklist; `i18n:check`, `i18n:english:check`, lint, typecheck, unit tests, and coverage all PASS on CI.
---
## Self-review (done before save)
**Spec coverage:**
- Architecture (strip inside panel, shared poll) → Tasks 4 + 6.
- Status mapping → Task 5.
- i18n parity → Tasks 2 + 3.
- Test coverage → Tasks 46 (each TDD'd).
- Deviations called out → Task 7 PR body Step 9.
- No new RPC, no Rust → no Rust task; Task 7 still runs format/typecheck/lint (no `pnpm rust:check` because zero Rust changed).
**Placeholder scan:** None — every code block is complete and copy-pasteable. No "TBD" / "TODO" / "fill in details". Translations are written out per locale.
**Type consistency:** `MemorySyncStatusRow` defined once in Task 1, referenced verbatim in Tasks 4 / 5 / 6. `classifyIntegration` / `providerIconChar` signatures match between definition (Task 5) and call sites (Task 6). `IntegrationHealth` type matches the union returned by `classifyIntegration`.
**Risks the engineer should know about:**
1. The pre-push hook can fail on a fresh worktree (`prettier: command not found`); Task 7 Step 8 calls this out and points at the memory note.
2. `pnpm i18n:english:check` is strict about Latin-script values — if a Spanish/Portuguese/Italian translation accidentally reuses the English word verbatim it will fail. The recommended translations above were chosen to avoid this; if a reviewer requests a different word, double-check it doesn't collide with English-only function words.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,486 +0,0 @@
# Telegram remote-control phase 2 — inline approvals
**Issue**: [#1805](https://github.com/tinyhumansai/openhuman/issues/1805) (phase 2 of N)
**Date**: 2026-05-23
**Status**: Approved for implementation
**Phase 1**: [PR #2249](https://github.com/tinyhumansai/openhuman/pull/2249) (merged) — `/status`, `/sessions`, `/new`, `/help`
---
## Goal
When `ApprovalGate` parks a tool call, every Telegram chat that has a session binding receives an inline-button prompt; whoever (or whatever surface) decides first wins, and all chats' prompts are edited to reflect the final state with attribution. Add a `/pending` command for on-demand recovery when the auto-broadcast was missed.
## Non-goals
- **Agent Q&A elicitation.** Free-form "ask the agent a question" already works via plain reply to the bound thread. This slice does not introduce a structured question/answer primitive.
- **Active expiry sweeping.** Buttons may go stale; tapping a stale button is the path that surfaces "expired" to the user. No background sweep task.
- **Real Telegram automation in CI.** Closest deterministic harness lives in the existing Rust integration test against `mockito`.
- **Other slices in #1805** (live activity ticks, `/abort`/`/detach`, scheduled tasks, file browsing, mode/model switching, worktree). Each will be its own spec → plan → PR cycle.
---
## Architecture
```
┌──────────────────┐
│ ApprovalGate │ intercepts external-effect tool calls
│ (existing) │ parks future on oneshot
└────────┬─────────┘
│ publishes
┌───────────────────────────────┐
│ DomainEvent::ApprovalRequested│
└─────────────┬─────────────────┘
│ event bus
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌──────────────────────┐ ┌────────────────┐
│ Desktop UI │ │ TelegramApproval- │ │ (future │
│ (existing) │ │ Subscriber (new) │ │ providers) │
└───────┬────────┘ └──────────┬───────────┘ └────────────────┘
│ │
│ │ for every bound chat:
│ │ sendMessage + inline_keyboard
│ │ record (chat_id, msg_id) in pending_map
│ ▼
│ ┌─────────────────┐
│ │ Telegram Bot │
│ │ API │
│ └────────┬────────┘
│ │ user taps button
│ ▼
│ ┌─────────────────────────┐
│ │ channel_recv.rs: │
│ │ callback_query handler │ ← allowlist re-check
│ │ (new branch) │
│ └────────────┬────────────┘
│ │ approval_decide(
│ │ request_id, decision,
│ │ actor=@user, surface="telegram")
▼ ▼
approval_decide RPC ───────┐
│ publishes
┌──────────────────────────────────┐
│ DomainEvent::ApprovalDecided │
│ (extended w/ decided_by_actor + │
│ decided_by_surface) │
└────────────────┬─────────────────┘
TelegramApprovalSubscriber.handle()
for each (chat_id, msg_id) in pending_map:
editMessageText("✓ decided by @who via X")
[remove inline_keyboard]
pending_map.remove(request_id)
```
### Key design choices
| Choice | Decision | Rationale |
|---|---|---|
| **Routing** | Broadcast to all chats with a session binding | Mirrors multi-device approval UX; first-decision-wins; `pending_map` carries the chat list for the follow-up edit. |
| **Pending → message-ids map** | In-memory inside `TelegramApprovalSubscriber` | After a core restart, taps on still-visible buttons fail gracefully ("Already decided" toast). Persistence would cost IO + schema churn for an edge case. |
| **Stale tap UX** | Toast via `answerCallbackQuery` + best-effort message edit | Operator never sees a "dead button" with no feedback. |
| **Cross-chat sync** | Subscribe to `ApprovalDecided`; edit every recorded posted message | Visibility into who acted and from where. |
| **Decider identity** | Extend `ApprovalDecided` event + `approval_decide` RPC + `ApprovalAuditEntry` with optional `decided_by_actor` / `decided_by_surface` | Without this, "decided by @who via X" is fiction. Surfaces other than Telegram default to `surface="desktop"`. |
| **Telegram API surface** | New `pub(crate)` methods on `TelegramChannel`: `send_with_inline_keyboard`, `edit_message_text`, `answer_callback_query` | Telegram-specific primitives stay in the provider; the generic `Channel` trait isn't dragged into approvals. |
| **Subscriber → callback bridge** | `static GLOBAL_APPROVAL_SUB: OnceLock<Arc<TelegramApprovalSubscriber>>` | Same singleton pattern as `ApprovalGate::try_global`; `channel_recv.rs` reaches the subscriber without restructuring the long-poll loop. |
| **`/pending` command** | One chat, on demand; reuses the same send + button + map registration path | Critical when the auto-broadcast was missed. Negligible extra code. |
| **Expiry handling** | Render `_Expires in Xm_` when `expires_at` is set; no background sweep; stale taps self-correct | Adding a sweep is its own bucket of work. |
---
## Files
### New
| File | Purpose | Approx LOC |
|---|---|---|
| `src/openhuman/channels/providers/telegram/approval_bus.rs` | `TelegramApprovalSubscriber``EventHandler` on `approval` domain. Holds `Arc<TelegramChannel>`, `workspace_dir`, `pending_map: parking_lot::Mutex<HashMap<String, Vec<PostedPrompt>>>`. Provides `decide_from_callback(...)` for `channel_recv.rs`. Exposes `set_global`/`try_global` over a `OnceLock`. | ~250 |
| `src/openhuman/channels/providers/telegram/approval_bus_tests.rs` | Unit tests for the subscriber (see Testing). | ~300 |
### Modified — Telegram provider
| File | Change |
|---|---|
| `src/openhuman/channels/providers/telegram/mod.rs` | `mod approval_bus;` + `pub use approval_bus::TelegramApprovalSubscriber;` + `#[cfg(test)] #[path = "approval_bus_tests.rs"] mod approval_bus_tests;`. |
| `src/openhuman/channels/providers/telegram/channel_send.rs` | Three new `pub(crate) async` methods on `TelegramChannel`: `send_with_inline_keyboard(chat_id, text, keyboard) -> Result<i64>`, `edit_message_text(chat_id, message_id, text, keyboard: Option<&Value>) -> Result<()>`, `answer_callback_query(callback_query_id, text, show_alert) -> Result<()>`. |
| `src/openhuman/channels/providers/telegram/channel_recv.rs` | New top-of-loop branch: `if let Some(cb) = update.get("callback_query") { self.handle_callback_query(cb).await; return; }`. New `handle_callback_query` method (allowlist re-check, parse `appr:<o\|a\|d>:<rid>`, dispatch). |
| `src/openhuman/channels/providers/telegram/remote_control.rs` | Add `TELEGRAM_CMD_PENDING = "/pending"` const + parser arm + `build_pending_response(...)` async builder. The builder uses the subscriber (`TelegramApprovalSubscriber::try_global()`) to broadcast prompts for currently-pending approvals into the calling chat only, registering them in `pending_map`. Help text updated. |
### Modified — core / approval domain
| File | Change |
|---|---|
| `src/core/event_bus/events.rs` | `DomainEvent::ApprovalDecided` gains `decided_by_actor: Option<String>`, `decided_by_surface: Option<String>`. |
| `src/openhuman/approval/types.rs` | `ApprovalAuditEntry` gains the same two optional fields. |
| `src/openhuman/approval/store.rs` | SQLite migration: `ALTER TABLE approval_audit ADD COLUMN decided_by_actor TEXT; ALTER TABLE approval_audit ADD COLUMN decided_by_surface TEXT;`. Read/write paths updated. |
| `src/openhuman/approval/gate.rs` | `decide()` gains `actor: Option<String>`, `surface: Option<String>`; threaded into the audit insert and the published event. |
| `src/openhuman/approval/rpc.rs` | `approval_decide` RPC gains two optional params. When both are absent, default `surface = Some("desktop")` for back-compat with older app builds. |
| `src/openhuman/approval/schemas.rs` | Schema definitions updated to reflect the optional params. |
### Modified — runtime wiring
| File | Change |
|---|---|
| `src/openhuman/channels/runtime/startup.rs` | After the existing `_telegram_remote_handle` block: if `channels_by_name.get("telegram")` downcasts to `TelegramChannel`, construct + `set_global` + subscribe the new `TelegramApprovalSubscriber`. On downcast failure, log error + continue (approvals fall back to desktop). |
A small surface change may be needed to make the `Arc<dyn Channel>``Arc<TelegramChannel>` conversion work. Acceptable options, ordered by preference:
1. **Helper free function** in `src/openhuman/channels/providers/telegram/mod.rs`:
```rust
pub fn try_downcast(arc: &Arc<dyn Channel>) -> Option<Arc<TelegramChannel>> { ... }
```
No trait changes; only the call site at `startup.rs` knows about it.
2. **`as_any` on `Channel` trait**, then `Arc::downcast` via `Arc<dyn Any>`. Slightly larger blast radius.
The plan should pick (1) unless implementation reveals a blocker.
### Modified — capability catalog
| File | Change |
|---|---|
| `src/openhuman/about_app/catalog.rs` | Extend the existing Telegram remote-control entry with `"Inline approval / deny / approve-always buttons"` + `"/pending"` capability strings. |
### Modified — frontend
| File | Change |
|---|---|
| `app/src/services/api/approvalApi.ts` (or wherever the `approval_decide` wrapper lives) | Add optional `actor` / `surface` params; default `surface: "desktop"` on desktop callers. |
| `app/src/services/api/__tests__/approvalApi.test.ts` | One test asserting the wrapper passes `surface: "desktop"` when caller omits both. |
| `app/src/components/channels/TelegramConfig.tsx` | A single status-line addition under existing remote-control copy: "Remote approvals: enabled (always-on when Telegram is connected)". No new toggle. |
| `app/src/components/channels/__tests__/TelegramConfig.test.tsx` | Snapshot/render assertion for the new copy. |
### Modified — E2E / integration
| File | Change |
|---|---|
| `tests/json_rpc_e2e.rs` | New scenario `approval_decide_accepts_actor_and_surface_optional_params` — call with both fields present and absent. |
| `src/openhuman/channels/tests/telegram_integration.rs` | New scenario `approval_round_trip_via_callback_query` — boot TelegramChannel + subscriber against mockito; bind one chat; publish synthetic `ApprovalRequested`; assert mock receives `sendMessage` with inline keyboard; feed synthetic `callback_query`; assert `approval_decide` → `ApprovalDecided` → mock receives `editMessageText` + `answerCallbackQuery`. |
---
## Data flow
### Subscriber registration (startup)
`src/openhuman/channels/runtime/startup.rs`, immediately after `_telegram_remote_handle`:
```rust
let _telegram_approval_handle = match channels_by_name.get("telegram")
.and_then(crate::openhuman::channels::providers::telegram::try_downcast)
{
Some(tg) => {
let sub = Arc::new(TelegramApprovalSubscriber::new(
tg,
config.workspace_dir.clone(),
));
TelegramApprovalSubscriber::set_global(Arc::clone(&sub));
let handle = bus.subscribe(sub);
tracing::debug!("[telegram-approval] registered TelegramApprovalSubscriber");
Some(handle)
}
None => {
if channels_by_name.contains_key("telegram") {
tracing::error!("[telegram-approval] channel present but downcast failed; approvals via Telegram disabled");
}
None
}
};
```
### `ApprovalRequested` → broadcast
```rust
async fn handle(&self, event: &DomainEvent) {
match event {
DomainEvent::ApprovalRequested { request_id, tool_name, action_summary, .. } => {
let bindings = self.load_bindings_snapshot();
if bindings.is_empty() {
tracing::debug!("[telegram-approval] no bound chats; skip id={request_id}");
return;
}
// Pre-insert empty vec so a racing ApprovalDecided event finds a
// (possibly partial) list rather than misses entirely.
self.pending_map.lock().insert(request_id.clone(), Vec::new());
let text = render_prompt(tool_name, action_summary, *expires_at);
let keyboard = approval_keyboard(request_id, tool_name);
for binding in bindings {
match self.channel.send_with_inline_keyboard(&binding.chat_id, &text, &keyboard).await {
Ok(message_id) => {
self.pending_map.lock()
.get_mut(request_id)
.map(|v| v.push(PostedPrompt { chat_id: binding.chat_id.clone(), message_id }));
}
Err(err) => tracing::warn!(
"[telegram-approval] send failed chat={} id={} err={}",
binding.chat_id, request_id, err,
),
}
}
}
DomainEvent::ApprovalDecided { request_id, decision, decided_by_actor, decided_by_surface, .. } => {
let posted = match self.pending_map.lock().remove(request_id) {
Some(p) => p,
None => {
tracing::debug!("[telegram-approval] decided event for unknown id={request_id}");
return;
}
};
let final_text = render_decided(decision, decided_by_actor.as_deref(), decided_by_surface.as_deref());
for p in posted {
if let Err(err) = self.channel.edit_message_text(&p.chat_id, p.message_id, &final_text, None).await {
tracing::warn!(
"[telegram-approval] edit failed chat={} msg={} err={}",
p.chat_id, p.message_id, err,
);
}
}
}
_ => {}
}
}
```
`approval_keyboard(request_id, tool_name)` returns three rows:
```json
[
[{"text": "✅ Approve once", "callback_data": "appr:o:<rid>"}],
[{"text": "♻ Always for <tool>", "callback_data": "appr:a:<rid>"}],
[{"text": "❌ Deny", "callback_data": "appr:d:<rid>"}]
]
```
`callback_data` is limited to 64 bytes by Telegram. `appr:<o|a|d>:<uuid>` is comfortably under.
### Inbound `callback_query` → decide
In `channel_recv.rs`:
```rust
if let Some(cb) = update.get("callback_query") {
if let Err(err) = self.handle_callback_query(cb).await {
tracing::warn!("[telegram][callback] handler error: {err}");
}
return;
}
// (existing message handler follows)
```
`handle_callback_query`:
1. Parse `id`, `from.username`, `from.id`, `message.chat.id`, `message.message_id`, `data`.
2. **Re-check allowlist** with `is_any_user_allowed([username_norm, id_norm])`. Non-allowlisted → `answer_callback_query(id, "Not authorized", show_alert=true)`; log; return.
3. Parse `data` as `appr:<o|a|d>:<rid>`. Malformed → toast "Invalid action"; return.
4. Map to `ApprovalDecision::{ApproveOnce, ApproveAlwaysForTool, Deny}`.
5. Call `approval_decide(rid, decision, actor=Some(@username), surface=Some("telegram"))`.
6. On `Err` whose source resolves to "no pending approval found" → toast "Already decided or expired"; best-effort `edit_message_text(chat_id, message_id, "⏱ already decided or expired", None)`.
7. On `Ok(_)` → toast "Decision recorded". The bus-driven edit (`ApprovalDecided` branch) updates message text in all bound chats.
### `/pending` command
In `remote_control.rs`:
1. `approval_list_pending().await?` → `Vec<PendingApproval>`.
2. Empty → reply `_No approvals pending._`.
3. Non-empty → for each row (capped at 5):
- **Dedupe per chat**: if `pending_map[request_id]` already has a `PostedPrompt` whose `chat_id` matches the calling chat, skip — the operator already has a live prompt in this chat from the auto-broadcast.
- Otherwise, construct prompt + keyboard exactly like the auto-broadcast path; call `subscriber.channel.send_with_inline_keyboard(...)` against **this chat only**; on success push `PostedPrompt` into `pending_map[request_id]` (creating the entry if absent — `/pending` may run for a request still parked but never broadcast, e.g. across a core restart).
4. If `> 5` rows: append a `_… and N more (open the desktop app to see all)_` line.
### Decider identity propagation
- `approval_decide` JSON-RPC body extends from `{ request_id, decision }` to `{ request_id, decision, actor?, surface? }`.
- `ApprovalGate::decide(...)`: signature gains `actor: Option<String>, surface: Option<String>`; values are passed through to `pending_approvals` row mutation + `approval_audit` row insert + the published `ApprovalDecided` event.
- SQLite migration: two new nullable TEXT columns on `approval_audit`. Existing rows keep `NULL` (which renders as "decided" with no "by" suffix — back-compat for any historical UI that re-reads audit).
---
## Error handling
| Failure | Behavior |
|---|---|
| `send_with_inline_keyboard` fails for one chat during broadcast | Log + skip; other chats still get a working prompt; `pending_map` records only successes. |
| All sends fail | Log error. Desktop UI still functions. No retry — single event, single attempt. |
| `edit_message_text` fails on a posted prompt | Log + continue editing the rest. Stale message remains; the next tap triggers the "already decided" self-correction path. |
| `approval_decide` returns "gate not installed" | Toast "Approvals disabled" + log. Defensive; subscriber would not have been registered if the gate is absent in a normal boot. |
| `approval_decide` returns "no pending approval found" | Toast "Already decided or expired" + best-effort edit on the tapped message. |
| Callback `data` malformed | Toast "Invalid action" + log. Poll loop continues. |
| Channel downcast fails at startup | Log error; subscriber not registered. Approvals fall back to desktop-only. Core continues. |
| SQLite migration fails (existing prod row count > 0, column add fails) | Surfaces during gate boot via the existing migration runner. No new logic here — leverage the existing migration error path. |
## Edge cases
| Case | Behavior |
|---|---|
| Chat binds via `/new` *after* `ApprovalRequested` was published | Not in bindings snapshot at broadcast time → no prompt. `/pending` covers the gap. |
| Chat is unbound between request and decision | Edits still post to the recorded `(chat_id, message_id)`; Telegram doesn't care about our `bindings` state. |
| Group chat: bound by allowlisted operator + has non-allowlisted members | Callback re-checks `from.username` / `from.id`; only allowlisted taps proceed. |
| Two operators tap simultaneously | First `approval_decide` wins; second hits "no pending approval found" → toast + self-correcting edit. |
| Same operator taps Approve-once then Deny in quick succession | Same path — second tap loses. |
| Core restart between request and decision | `pending_map` is lost. Old buttons in Telegram still call back. Allowlist still works (loaded from config). `approval_decide` returns "no pending approval found" → toast + tap-local edit only. Other chats stay stale. **Acceptable**: the parked future itself was lost on restart already; this slice doesn't change that. |
| `expires_at` reached while message is live | No active expiry sweep. The next tap returns "no pending approval found" and we render `⏱ already decided or expired`. |
| `mention_only` mode is on | Does not apply to `callback_query` — those are direct button taps, not message mentions. Always processed if allowlist passes. |
| `pairing_code_active` (no chats bound yet) | Broadcast path early-returns (`bindings.is_empty()`). Future approvals reach chats once binding completes. |
| Concurrent `ApprovalRequested` + `ApprovalDecided` ordering | Mitigated by pre-inserting an empty `Vec` into `pending_map` *before* awaited sends, so a `Decided` arriving mid-broadcast still finds an entry (possibly partial) to drain and remove. |
## Security
| Concern | Mitigation |
|---|---|
| Non-allowlisted user taps a button | Re-check `from.username` + `from.id` against the allowlist on every callback. Non-allowlisted → toast "Not authorized" + log. Identical to the message-path check. |
| `callback_data` spoofing | Telegram guarantees `from` on `callback_query` is the real user; allowlist re-check covers spoofed senders. `request_id` is still validated against the gate, which checks session + existence. |
| PII / secrets in prompt body | `action_summary` and `args_redacted` are already scrubbed by `approval/redact.rs`. The prompt body uses only `action_summary` — no raw args, no chat content. |
| Logging | Stable prefix `[telegram-approval]`. Log `request_id`, `tool_name`, `decision`, `chat_id`, `actor`, `surface`. Never log message bodies, args, or full `action_summary`. |
| Bot token in logs | Existing telegram code redacts; new send helpers reuse the existing `api_url(...)` helper, which already keeps the token out of error strings. |
| Callback flood from a misbehaving client | Allowlist gates this. No per-user rate limiting in this slice — out of scope. |
---
## Testing
Target: ≥80% diff coverage on changed lines. Behavior over implementation. No real network.
### Rust unit — `approval_bus_tests.rs` (new)
Built on a fake `TelegramChannel` (constructor takes a `mockito` URL; send helpers wrap `reqwest` against it — same pattern in existing `channel_tests.rs`). Each test uses an isolated `tempdir` for `workspace_dir` + `TelegramSessionStore`.
| Test | What it proves |
|---|---|
| `broadcasts_to_all_bound_chats` | Two bound chats; publish `ApprovalRequested`; mock receives two `sendMessage` calls with matching inline_keyboard; `pending_map` has 2 entries. |
| `no_bound_chats_skips_broadcast` | Zero bindings → zero sends, no map entry, no panic. |
| `partial_send_failure_still_records_successes` | First chat 200, second chat 500. `pending_map` has only the successful chat. Warn logged. |
| `pre_insert_avoids_lost_decided_race` | Confirm `pending_map.insert(rid, Vec::new())` runs **before** awaited sends; simulate `ApprovalDecided` mid-broadcast and assert no missed-entry log. |
| `edits_all_chats_on_decided` | Two bound; broadcast; `ApprovalDecided{actor=Some("@op"), surface=Some("telegram")}` → two `editMessageText` calls; body contains "approved by @op via telegram"; keyboard removed. |
| `decided_event_for_unknown_id_is_noop` | `Decided` for an id not in `pending_map` → zero sends, debug log. |
| `decided_event_renders_surface_unknown_gracefully` | `actor=None, surface=None` → body says "approved" with no "by" suffix. |
| `expires_in_rendered_when_set` | `ApprovalRequested` with `expires_at = now+300s` → outgoing text contains `Expires in 5m`. With `expires_at=None` → no expiry line. |
| `prompt_text_does_not_leak_args` | Pass `action_summary` containing only a redacted summary; assert outgoing text contains *only* that, no `args_redacted`. |
### Rust unit — `channel_recv` callback_query (extended `channel_tests.rs`)
| Test | What it proves |
|---|---|
| `callback_query_from_allowed_user_dispatches_decide` | Build update with `data="appr:o:<rid>"`, allowed user; stub `approval_decide`; assert one call with `decision=ApproveOnce, actor=Some("@user"), surface=Some("telegram")`. |
| `callback_query_from_unauthorized_user_is_rejected` | Same but unauthorized `from`; assert zero `approval_decide` calls, one `answerCallbackQuery` text≈"Not authorized". |
| `callback_query_malformed_data_toasts_invalid` | `data="garbage"` → no decide, one toast text≈"Invalid". |
| `callback_query_already_decided_toasts_and_attempts_edit` | Stub `approval_decide` → `Err("no pending approval found")` → one toast + one `editMessageText` on the tapped (chat_id, message_id). |
| `callback_query_does_not_fall_through_to_message_handler` | Update with `callback_query` and no `message.text` → existing inbound-message path not invoked; no `ChannelMessageReceived`. |
| `callback_query_allowlist_lookup_uses_id_and_username` | Username not in allowlist but `from.id` is → permitted. Mirrors message-path behavior. |
### Rust unit — `remote_control` `/pending` (extended `remote_control_tests.rs`)
| Test | What it proves |
|---|---|
| `pending_command_with_no_approvals_replies_empty` | Empty `approval_list_pending` → reply ≈"No approvals pending". |
| `pending_command_renders_each_row_with_buttons` | Three pending → three sends with full keyboard; each recorded in `pending_map`. |
| `pending_command_caps_at_5_rows` | Ten pending → first 5 + `_… and 5 more_` line. |
| `pending_command_is_chat_local_not_broadcast` | Calling `/pending` from chat A while B is bound → only A receives prompts. |
| `pending_command_dedupes_per_chat` | `pending_map[rid]` already has a PostedPrompt for chat A; `/pending` in chat A → that row is skipped (zero new sends for that rid), other rids still rendered. |
### Approval-domain tests (extended)
| File | Test |
|---|---|
| `src/openhuman/approval/rpc.rs` (or new `rpc_tests.rs`) | `approval_decide_threads_actor_and_surface_into_event` — call with `Some` values; capture `ApprovalDecided`; assert both propagate. |
| `src/openhuman/approval/store.rs` | `audit_row_round_trips_actor_and_surface`; `audit_row_round_trips_nullable_actor_and_surface`. |
| `src/openhuman/approval/gate.rs` tests | `decide_records_actor_and_surface_to_audit` — end-to-end through the gate. |
### JSON-RPC E2E — `tests/json_rpc_e2e.rs`
| Scenario | What it proves |
|---|---|
| `approval_decide_accepts_actor_and_surface_optional_params` | Hit `approval_decide` over JSON-RPC with both fields, `Some` and absent. Both succeed. Existing response shape preserved (additive only). |
### Frontend tests
| File | Test |
|---|---|
| `app/src/services/api/__tests__/approvalApi.test.ts` | `approval_decide_passes_surface_desktop_by_default` — caller omits both → wrapper sends `surface: "desktop"`. |
| `app/src/components/channels/__tests__/TelegramConfig.test.tsx` | New "Remote approvals: enabled …" copy renders when Telegram is connected. |
### Deterministic Telegram E2E — `telegram_integration.rs`
`approval_round_trip_via_callback_query`:
1. Boot TelegramChannel + TelegramApprovalSubscriber against `mockito`.
2. Bind one chat via `/new` (mock returns success).
3. Publish synthetic `ApprovalRequested`.
4. Assert mock received `sendMessage` with inline_keyboard.
5. Feed synthetic `callback_query` with valid `data` from allowed user.
6. Assert `approval_decide` invoked → `ApprovalDecided` published → mock received `editMessageText` + `answerCallbackQuery`.
This is the closest deterministic equivalent to real Telegram automation the project supports today.
### Coverage estimate
| Area | New lines (approx) | Tested lines |
|---|---|---|
| `approval_bus.rs` | ~250 | ≥240 |
| `channel_send.rs` (3 new helpers) | ~120 | ≥110 |
| `channel_recv.rs` callback branch | ~80 | ≥75 |
| `remote_control.rs` `/pending` | ~60 | ≥55 |
| `approval/gate.rs` + `store.rs` + `rpc.rs` + event + schemas | ~70 | ≥65 |
Diff coverage should land comfortably above 80%; the uncovered tail is defensive error-logging arms.
---
## Acceptance criteria (mapping to #1805)
This slice satisfies the following acceptance criteria from #1805:
- ☑ "Users can answer agent questions and **approve/deny permission requests** from Telegram inline." (permission half only — Q&A is non-goal for this slice.)
- ☑ "Core functionality is exposed through registered controllers and schemas, not Telegram-specific branches in transport adapters." — Telegram only adds an event-bus subscriber that calls existing `approval_decide` RPC; no Telegram branches in `approval/` domain.
- ☑ "`src/openhuman/about_app/` is updated so the capability catalog reflects the expanded Telegram feature set."
- ☑ "New/changed flows include substantial debug logging in Rust and app code with grep-friendly prefixes and no secret leakage." — `[telegram-approval]` prefix throughout.
- ☑ "Rust unit/integration coverage is added for the new Telegram control flows and JSON-RPC/controller surfaces."
- ☑ "App unit tests are added for Telegram configuration/management UI changes." (minimal, scoped to actual changes.)
- ☑ "Desktop E2E coverage … or the nearest deterministic harness equivalent" — covered by the `telegram_integration.rs` scenario.
- ☑ "Diff coverage ≥ 80%."
Out of scope for this slice (to be addressed in later phases):
- live activity ticks during runs / `/abort` / `/detach`
- `/task` / `/tasklist`
- `/files` / `/ls`
- inbound attachments / structured outbound file delivery
- `/models` / `/mode`
- `/projects` / `/worktree`
---
## Implementation order (for the plan)
1. **Core/approval changes first** — extend event, RPC, gate, audit, store + tests. Independently mergeable.
2. **Telegram send helpers** — `send_with_inline_keyboard`, `edit_message_text`, `answer_callback_query` + tests against mockito.
3. **`TelegramApprovalSubscriber`** + unit tests.
4. **`channel_recv.rs` callback branch** + unit tests.
5. **`/pending` command** + unit tests.
6. **Wiring in `startup.rs`** + downcast helper.
7. **Catalog update** + frontend touch + tests.
8. **`telegram_integration.rs` round-trip** + `json_rpc_e2e.rs` scenario.
---
## Risks
| Risk | Mitigation |
|---|---|
| Existing desktop frontend calls `approval_decide` without the new params | Server defaults `surface = Some("desktop")` when both are absent. Frontend update is additive. |
| `Arc<dyn Channel>` → `Arc<TelegramChannel>` downcast may need a small trait change | Plan covers a `try_downcast` helper option first; falls back to `as_any` only if needed. |
| Real Telegram users tap stale buttons after core restart | Acceptable — "already decided or expired" toast + best-effort edit covers it. |
| `callback_data` length cap (64 bytes) is exceeded by long request_ids | Request ids are uuids (36 bytes); `appr:<o|a|d>:` adds 7. Total 43. Comfortable headroom. Validate at construction time in `approval_keyboard` to fail loud if this ever changes. |
---
## Open questions
None at design approval. Implementation may surface:
- Exact mockito URL injection pattern in the new `approval_bus_tests.rs` — defer to plan-writing to confirm the existing test harness shape carries over.
- Whether the frontend `approval_decide` wrapper currently lives in `services/api/` or somewhere else — defer to a brief grep at plan-writing time.
@@ -1,380 +0,0 @@
# Global Push-to-Talk Hotkey — Design
**Issue:** [tinyhumansai/openhuman#3090](https://github.com/tinyhumansai/openhuman/issues/3090) — "Global push-to-talk keybind + screen share while tabbed out / in background."
**Scope of this spec:** the *push-to-talk* half only. Background screen capture for the agent is acknowledged in the issue and tracked as a follow-up PR — same domain (voice / agent context), different surface area (host-screen sampling, fullscreen-game compatibility, image-token budget). Keeping them separate keeps each PR reviewable and coverage-gateable.
**Outcome:** the user holds a configurable global hotkey while OpenHuman is *not* the focused window (mid-game, in their IDE, on a Slack call), speaks, releases the key, and the agent answers via TTS — without OpenHuman ever stealing focus.
---
## Goals
- A user-configurable hold-to-talk hotkey that works while OpenHuman is in the background.
- Mic opens on press, closes on release; transcript is auto-posted to the active chat thread and the agent's reply is spoken aloud.
- Audible + visual feedback (chime + small always-on-top overlay) so the user knows the mic is hot without alt-tabbing.
- Works on macOS, Windows, and Linux/X11 in v1. Wayland: documented unsupported with a clear in-app message.
## Non-goals (v1)
- Background screen capture for the agent. (Follow-up issue spawned from #3090.)
- Streaming partial transcripts during the hold.
- Per-thread PTT routing (always routes to the active thread).
- A DXGI-exclusive-fullscreen overlay workaround. (Documented caveat only; chime still plays.)
- Toggle-style PTT (we ship hold-to-talk only — the existing dictation toggle remains for press-once-press-again users).
---
## Architecture overview
```
[User holds hotkey]
[Tauri shell: tauri-plugin-global-shortcut]
│ ShortcutState::Pressed
[app/src-tauri/src/ptt_hotkeys.rs]
│ emit("ptt://start", { session_id })
[app/src/services/pttService.ts] ─┐
│ voice/audio_capture.start │ hold phase
│ playChime("open") │
│ invoke("show_ptt_overlay", { active }) │
│ armWatchdog(10s) │
─┘
[User releases hotkey]
│ ShortcutState::Released
[ptt_hotkeys.rs] emit("ptt://stop", { session_id })
[pttService.onStop]
│ voice/audio_capture.finalize → Buffer
│ playChime("close") + hide overlay
│ dictationListener.transcribe(buf) → text
│ chatRuntime.sendMessage({ text, speakReply: true, source: "ptt" })
[Core: openhuman.channel_web_chat]
│ normal agent turn
│ on assistant final text:
│ voice::reply_speech.synthesize_and_play(text) // if speak_reply
[User hears reply; OpenHuman window state never changes]
```
The bulk of the work is in the **Tauri shell** (hotkey + overlay window) and the **renderer service layer** (state machine + glue). The Rust core gets exactly one additive change: a `speak_reply: bool` flag on `channel.web_chat` so TTS reply routing doesn't require the renderer to be focused or even running its normal chat UI.
---
## Components
### Tauri shell — `app/src-tauri/src/`
#### `ptt_hotkeys.rs` *(new)*
Owns global hotkey registration for PTT. Mirrors `dictation_hotkeys.rs` in shape, with two key differences: it listens for **both** `Pressed` and `Released`, and rejects pure-modifier shortcuts.
```rust
pub(crate) struct PttHotkeyState {
pub(crate) shortcut: Mutex<Vec<String>>, // expanded variants registered
pub(crate) is_held: AtomicBool, // CAS-guarded press/release
pub(crate) session_counter: AtomicU64,
}
pub(crate) fn expand_ptt_shortcuts(shortcut: &str) -> Result<Vec<String>, PttError>;
// - returns Err(EmptyShortcut) if trimmed empty
// - returns Err(ModifierOnlyShortcut) if every token is a modifier (Ctrl/Cmd/Shift/Alt/Meta)
// - returns Err(InvalidShortcut(...)) if the plugin parser rejects it
// - otherwise returns 1 or 2 expanded variants (macOS CmdOrCtrl → [Cmd, Ctrl])
pub(crate) enum PttError {
EmptyShortcut,
ModifierOnlyShortcut,
InvalidShortcut(String),
AccessibilityRequired, // macOS
ShortcutInUse(String), // Windows
UnsupportedOnWayland,
ConflictsWithDictation(String),
RegistrationFailed(String),
}
```
#### `lib.rs` — two new IPC commands
```rust
#[tauri::command]
async fn register_ptt_hotkey(app: AppHandle<AppRuntime>, shortcut: String) -> Result<(), String>;
#[tauri::command]
async fn unregister_ptt_hotkey(app: AppHandle<AppRuntime>) -> Result<(), String>;
```
Behavior on `register_ptt_hotkey`:
1. Expand & validate via `expand_ptt_shortcuts`.
2. Check overlap with the currently-registered dictation shortcut(s); on overlap return `ConflictsWithDictation`.
3. Unregister any previously-registered PTT shortcut (rollback-safe — same pattern as the dictation registration).
4. Register each expanded variant with a closure that:
- On `Pressed`: CAS `is_held: false → true`; on success, increment `session_counter` and emit `ptt://start { session_id }`. On failure (CAS lost — auto-repeat or stuck state), drop.
- On `Released`: CAS `is_held: true → false`; on success, emit `ptt://stop { session_id }` with the *current* counter value. On failure, drop.
5. Persist the registered variants in `PttHotkeyState`.
`unregister_ptt_hotkey` unregisters all currently-registered variants and clears state. Also called on shutdown (`unregister_all` already covered by the plugin's drop).
#### `ptt_overlay.rs` *(new)* — dedicated overlay window
Lazy-create-on-first-register, destroyed on `unregister`. Window config:
| Field | Value |
| --- | --- |
| `label` | `"ptt-overlay"` |
| `url` | `/#/ptt-overlay` (HashRouter route, mounted only in this window) |
| `decorations` | `false` |
| `transparent` | `true` |
| `always_on_top` | `true` |
| `skip_taskbar` | `true` |
| `focus` | `false` (never accepts focus) |
| `resizable` | `false` |
| `shadow` | `false` |
| `visible_on_all_workspaces` | `true` |
| `accept_first_mouse` | `false` |
| `size` | `160 × 56` |
| `position` | bottom-right of primary display, 24px inset (hard-coded in v1) |
IPC command: `show_ptt_overlay({ active: bool, session_id: u64 })` — hides/shows the window with a 250ms fade on close. Window-local React state in `/#/ptt-overlay` toggles a pulsing red dot when `active: true`.
### Rust core — `src/openhuman/`
#### `voice/bus.rs` *(new)*
Per the canonical module shape, the voice domain currently has no `bus.rs`. Add one with a single subscriber-less event publisher and a new variant on `DomainEvent`:
```rust
// in src/core/event_bus/events.rs
pub enum VoiceEvent {
PttTranscriptCommitted {
thread_id: ThreadId,
session_id: u64,
text_len: usize, // never log raw transcript
held_ms: u64,
finalized_by_watchdog: bool,
},
// ...future variants
}
// in DomainEvent
Voice(VoiceEvent),
```
Subscribers will be added in the follow-up screen-capture PR (the screen-intelligence domain will hook here to grab a frame when a PTT turn commits). For v1 we publish, nobody subscribes — the test asserts publish reaches a test subscriber.
#### Chat-send schema — `speak_reply` flag
The user→agent ingress RPC is **`openhuman.channel_web_chat`** (web channel provider — `src/openhuman/channels/providers/web.rs`, schema in `schemas("chat")`, handler `channel_web_chat`, dispatch through `start_chat`). The frontend already calls this from `app/src/services/chatService.ts::chatSend`. Three additive optional fields:
```rust
// In the channel.web_chat input schema (web.rs schemas())
#[serde(default)]
pub speak_reply: Option<bool>,
#[serde(default)]
pub source: Option<String>, // "ptt" | "dictation" | "type" | ...
#[serde(default)]
pub session_id: Option<u64>, // PTT correlation key
```
Non-breaking — all fields `Option`. The flags flow through `channel_web_chat → start_chat → spawn_progress_bridge`. The progress bridge buffers `AgentProgress::TextDelta` chunks during the turn; on `AgentProgress::TurnCompleted`, if `speak_reply == Some(true)`, it calls `voice::reply_speech::synthesize_and_play(buffered_text).await`. This is the **only** Rust-core code path change beyond the schema and the bus event.
`source` and `session_id` are persisted on the user message metadata (via the message-record path already used by `start_chat`) and included in the `VoiceEvent::PttTranscriptCommitted` bus event for the screen-capture follow-up PR.
#### `about_app` capability catalog
Add entry:
```rust
Capability {
id: "voice.ptt",
label: "Global push-to-talk",
supported_on: &[Platform::MacOS, Platform::Windows, Platform::LinuxX11],
requires: &["microphone", "global_shortcut"],
}
```
### Frontend — `app/src/`
#### `services/pttService.ts` *(new singleton)*
State machine:
```
Idle ──[ptt://start]──▶ Capturing ──[ptt://stop]──▶ Finalizing ──▶ Idle
▲ │
│ ├──[10s no stop]──▶ Finalizing (watchdog=true)
│ │
│ └──[mic-fail / preempt / register]──▶ Aborted ──▶ Idle
```
API surface:
```ts
interface PttService {
init(): void; // subscribes to Tauri ptt://* events
destroy(): void;
// exposed for tests:
onStart(session_id: number): Promise<void>;
onStop(session_id: number): Promise<void>;
cancel(reason: "preempted_by_ptt" | "mic_failure" | "user_cancel"): void;
}
```
`onStart` (in order):
1. If a session is already active → call `cancel("preempted_by_ptt")`.
2. `playChime("open")`.
3. `invoke("show_ptt_overlay", { active: true, session_id })`.
4. `voice/audio_capture.start({ session_tag: "ptt:" + session_id })`.
5. `armWatchdog(10_000, () => this.onStop(session_id))`.
`onStop`:
1. Disarm watchdog.
2. `const buf = await voice/audio_capture.finalize()`.
3. `playChime("close")`.
4. `invoke("show_ptt_overlay", { active: false, session_id })`.
5. If `buf.duration_ms < 250` → drop session, play `"no-speech"` double-click chime, log `dropped_reason: "empty_audio"`, return.
6. `const text = await dictationListener.transcribe(buf)`.
7. If `!text.trim()` → drop, log `dropped_reason: "empty_transcript"`, return.
8. Resolve `activeThreadId`:
- If `chatRuntime.activeThread` exists → use it.
- Else → create a new thread titled `"Voice"` via `openhuman.thread_create`, mark `source: "ptt"`, use its ID.
9. `chatRuntime.sendMessage({ threadId, body: text, metadata: { source: "ptt", session_id }, speakReply: state.ptt.speakReplies })`.
10. Zero the audio buffer.
`cancel`:
- Disarm watchdog, finalize-and-discard the audio buffer (zero it), hide overlay, play error chime, log with reason. No chat message posted.
Errors during the session — handled per the table in **§ Error handling** below.
#### `store/slices/ptt.ts` *(new redux slice)*
```ts
interface PttState {
shortcut: string | null; // null = unbound (default)
speakReplies: boolean; // default true
showOverlay: boolean; // default true
isHeld: boolean; // not persisted
}
```
Persisted (except `isHeld`) via the existing redux-persist config. Re-registers the hotkey on rehydration via a sibling `useEffect` to the existing dictation init.
#### `pages/settings/voice/PttSettingsPanel.tsx` *(new)*
- Hotkey-capture widget (same component family as the dictation key picker).
- Toggle: "Speak agent replies" (`speakReplies`).
- Toggle: "Show overlay while held" (`showOverlay`).
- Inline help: "Push-to-talk is off — pick a hotkey to enable." when `shortcut == null`.
- Inline error: surfaces `PttError::ConflictsWithDictation`, `ShortcutInUse`, `AccessibilityRequired` (with a "Open Accessibility settings" button on macOS), `UnsupportedOnWayland`.
- Inline hint: "In exclusive-fullscreen games the overlay won't render — you'll only hear the chime. Switch to borderless fullscreen for the overlay."
#### `pages/PttOverlayPage.tsx` *(new — rendered only in the overlay window)*
Borderless 160×56 region: small mic glyph, label ("Listening…"), pulsing red dot when `state.active`. Reads `active` from a local React state updated by a `useEffect` that listens for `show_ptt_overlay`-relayed events. No redux access — the overlay window has its own React root.
#### `ChatRuntimeProvider` — forward `speak_reply`
`chatService.chatSend` (already the single call site for `openhuman.channel_web_chat`) accepts `speakReply?: boolean`, `source?: string`, `sessionId?: number` and forwards them as the new optional fields. `ChatRuntimeProvider`'s `sendMessage` plumbs them through from `pttService`.
#### Chimes
- `app/src/assets/audio/ptt-open.wav` — short rising tone, ~80ms.
- `app/src/assets/audio/ptt-close.wav` — short falling tone, ~80ms.
- `app/src/assets/audio/ptt-error.wav` — double-click, ~120ms.
- `app/src/assets/audio/README.md` — CC0 attribution.
LUFS-normalized to roughly match the existing in-app notification sound. Played via a plain `Audio` element from `pttService`.
#### i18n
New keys under a `pttSettings` / `pttOverlay` namespace in `app/src/lib/i18n/en.ts`, real translations added to all 13 non-English locale files (`ar`, `bn`, `de`, `es`, `fr`, `hi`, `id`, `it`, `ko`, `pl`, `pt`, `ru`, `zh-CN`). `pnpm i18n:check` and `pnpm i18n:english:check` gate this.
---
## Data flow / sequence diagram
See the architecture overview above. The key invariants:
- **No focus stealing.** No window is `show()`-ed with focus; `show_ptt_overlay` shows a `focus: false` window. The agent reply plays via TTS without any window-state mutation.
- **Single mic at a time.** `voice::audio_capture` enforces this. PTT preempts in-flight dictation; dictation cannot start during a PTT session.
- **Session ID is the correlation key.** Logged in shell + renderer + bus event + chat metadata.
---
## Error handling
| Failure | Behavior |
| --- | --- |
| Mic permission denied (`MicPermissionDenied`) | Error chime, hide overlay, log `[ptt] mic_denied`. Next time the user opens `/settings/voice`, a sticky banner links to OS mic settings. No mid-game modal. |
| Mic stream drops mid-session (USB unplug) | `cancel("mic_failure")`. No chat message posted. |
| STT call fails (network / model timeout) | Post message anyway as `[Voice — transcription failed]` so the user has a breadcrumb. Subsequent agent turn handles it normally. |
| Agent turn errors | Existing chat-error UI. TTS reply just doesn't play. Overlay already hidden by this point. |
| `ptt://stop` never arrives (OS swallowed release) | 10s watchdog finalizes. Session tagged `finalized_by_watchdog: true`. Logged at `warn`. |
| App backgrounded during hold | Hotkey still fires (global). Overlay still shows. Chime still plays. By design. |
| Empty / sub-threshold audio (< 250ms) | Drop session, play `no-speech` chime, log `dropped_reason: "empty_audio"`. No message posted. |
| Empty transcript (STT returned blank) | Same as above with `dropped_reason: "empty_transcript"`. |
| Shortcut conflict with dictation | Registration returns `ConflictsWithDictation`. Settings panel shows the inline error. |
| Wayland session | `UnsupportedOnWayland`. Settings panel surfaces a clear message. Logged once per session. |
**Logging** (per the debug-logging rule): all logs use `[ptt]` prefix. Fields per session: `session_id`, `shortcut`, `held_ms`, `transcript_len`, `dropped_reason`, `finalized_by_watchdog`. PII-safe — never log transcript text or audio buffers, only lengths/durations. Audio buffers are zeroed after finalize.
**Telemetry**: one new analytics event `ptt_session` mirroring the log fields (no transcript), gated by the existing analytics opt-in.
---
## Configuration
- **No `Config` TOML schema change.** All PTT settings live in the renderer's `ptt` redux slice (persisted), mirroring how dictation is configured today.
- **Default `shortcut: null`** (unbound). No hard-coded default key — every possible default conflicts with something common.
- **Default `speakReplies: true`**, **`showOverlay: true`**.
- **Boot path:** on rehydration, if `state.ptt.shortcut` is non-null, call `register_ptt_hotkey`. On settings change, unregister-then-register. Independent of the existing dictation init.
---
## Migration
Brand-new state. No migration. Existing users on `0.53.45+` see the new `/settings/voice` PTT section after upgrade with everything default-off until they bind a key.
---
## Testing
| Layer | What | Where |
| --- | --- | --- |
| Rust unit | `expand_ptt_shortcuts`: empty, modifier-only, valid combos, `CmdOrCtrl` expansion (dual-variant on macOS, single on Win/Linux) | `app/src-tauri/src/ptt_hotkeys.rs` inline `#[cfg(test)]` |
| Rust unit | `speak_reply` / `source` / `session_id` round-trip through `channel.web_chat` schema serde; default behavior unchanged when all omitted | `src/openhuman/channels/providers/web_tests.rs` |
| Rust unit | `DomainEvent::Voice::PttTranscriptCommitted` publishes; test subscriber receives it | `src/openhuman/voice/bus.rs` inline tests |
| Rust E2E | `tests/json_rpc_e2e.rs` — call `channel.web_chat` with `speak_reply: true` and assert `reply_speech::synthesize_and_play` is invoked via a test seam at the progress-bridge's `TurnCompleted` boundary | `tests/json_rpc_e2e.rs` extension |
| Vitest unit | `pttService` state machine: start→stop happy path, watchdog timeout, empty-audio drop, empty-transcript drop, dictation-preempt, double-press idempotency, mic-permission-denied path | `app/src/services/pttService.test.ts` (new) |
| Vitest unit | `ptt` redux slice: shortcut set/clear, toggle settings, rehydration | `app/src/store/slices/ptt.test.ts` (new) |
| Vitest unit | `PttSettingsPanel` — render, hotkey capture, conflict-with-dictation error, mic-denied banner, Wayland banner | `app/src/pages/settings/voice/PttSettingsPanel.test.tsx` (new) |
| Vitest unit | `PttOverlayPage` — renders idle vs active states, listens for active event | `app/src/pages/PttOverlayPage.test.tsx` (new) |
| i18n gate | All new keys present in all 13 locales, no untranslated English values | `pnpm i18n:check` + `pnpm i18n:english:check` (existing CI) |
| WDIO E2E | Desktop spec: register a hotkey via settings UI, simulate the hotkey via `tauri-driver` key injection, assert overlay window appears, assert chat thread receives a message. STT mocked via the shared mock backend returning a fixed transcript. | `app/test/e2e/specs/ptt-flow.spec.ts` (new) |
| Manual smoke | Hold-while-game-in-foreground on macOS + Windows; mic permission denied flow; Wayland fallback message | PR body checklist |
**Coverage gate.** Every changed line in the new files + the `channel.web_chat` schema delta ships with ≥ 80% diff coverage per the existing merge gate. Untested escape valves (the real `Audio.play()` call, the real `tauri-driver` key injection) are isolated behind thin wrappers that can be mocked.
---
## Out of scope (named explicitly)
- **Background screen capture for the agent** — separate follow-up PR off the same issue.
- **PTT-while-dictation-mid-flight** polish beyond "preempt with reason."
- **DXGI exclusive-fullscreen overlay rendering** — documented caveat only.
- **Streaming partial transcripts during hold.**
- **Per-thread PTT routing** (v1 always uses active thread; if none, creates a `"Voice"` thread).
- **Native platform overlays** (NSWindow / Win32 layered / X11 override-redirect) — Tauri overlay window covers v1 needs.
- **PTT toggle-mode** — out; dictation toggle covers that pattern already.
---
## Open questions
None at spec time. If implementation surfaces blockers (e.g. `tauri-plugin-global-shortcut` `Released` semantics regress on a specific OS version), revisit with a small spec amendment rather than a silent design drift.
@@ -1,122 +0,0 @@
# Memory Tree — per-integration health strip (issue #2763)
**Status:** approved, ready for implementation plan
**Issue:** [tinyhumansai/openhuman#2763](https://github.com/tinyhumansai/openhuman/issues/2763)
**Parent umbrella:** #1856 Part 3
**Depends on (already merged):** #2719 (status panel), #1250 (per-source sync status)
## Goal
Give operators a quick at-a-glance per-integration health view, directly under the four-tile Memory Tree status panel, so a stalled / low-volume tree can be traced to a single integration (e.g. Gmail) rather than the pipeline as a whole.
## Non-goals
- Replacing or shrinking `MemorySourcesRegistry` (the full sources list stays below as-is — it owns add / sync / remove flows).
- Per-provider `Error` attribution. This needs new core work (job → source linkage) and is **deferred** to a follow-up issue tracked in the PR body.
- Team-scoped wiki silos (Part 2 of #1856 — blocked on FR9, not in scope here).
## Architecture
Frontend-only diff. Reuses the existing `openhuman.memory_sync_status_list` RPC (shipped by #1250) as the data source. The 575-line `MemorySourcesRegistry` remains as the canonical configurable-sources view; this strip is a smaller, health-focused readout colocated with the pipeline-status tiles.
```text
┌─────────────────────────────────────────────────────────────────┐
│ MemoryTreeStatusPanel │
│ ┌─Status─┬─LastSync─┬─Chunks─┬─Wiki──┐ │
│ │ … │ … │ … │ … │ │
│ └────────┴──────────┴────────┴───────┘ │
│ │
│ ┌── Per-integration health ──────────────────────────────────┐ │
│ │ [icon] slack 5,231 chunks · 3 min ago ● Active │ │
│ │ [icon] gmail 842 chunks · 2 hr ago ● Stale │ │
│ │ [icon] notion 45 chunks · 5 d ago ● Stale │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Auto-sync toggle ──────────────────────────────[switch]──┐ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ MemorySourcesRegistry (existing, unchanged) │
└─────────────────────────────────────────────────────────────────┘
```
## Data flow
1. `useMemoryTreeStatus` (existing hook in `MemoryTreeStatusPanel.tsx`) extends `fetchOnce` to call both:
- `memoryTreePipelineStatus()` (existing)
- `memorySyncStatusList()` (existing — wraps `openhuman.memory_sync_status_list`)
in parallel via `Promise.all`. Returned object gains an `integrations: MemorySyncStatus[]` field.
2. Adaptive polling (1.5s while syncing, 4s otherwise) — the existing cadence — drives both fetches. One timer, one re-render.
3. A new internal sub-component `<IntegrationHealthStrip>` (same file, not exported) consumes `integrations` and renders the list. Lives inside `MemoryTreeStatusPanel` between the tile grid and the auto-sync toggle row.
## Status mapping (pure TS, no core change)
```ts
type IntegrationHealth = 'active' | 'stale';
function classifyIntegration(s: MemorySyncStatus): IntegrationHealth {
return s.freshness === 'active' ? 'active' : 'stale';
}
```
- `freshness === 'active'`**Active** (chunk within last 30 s)
- `freshness === 'recent' | 'idle'`**Stale**
- **Error** state intentionally omitted; see "Deferred" below.
## UI details
- One row per `MemorySyncStatus`. Icon from a small built-in `PROVIDER_ICONS` map inside `MemoryTreeStatusPanel.tsx` (keyed by sync-provider name — `slack` / `gmail` / `notion` / …, distinct from `SOURCE_KIND_ICONS` which keys by `SourceKind`); fallback to a generic `🔌` glyph for unknown providers.
- Provider name: friendly label via `SOURCE_KIND_LABEL_KEYS[provider]` when present, else the raw `provider` string.
- "5,231 chunks · 3 min ago" — chunk count + relative time, reusing the existing `formatRelativeMs()` helper from `MemoryTreeStatusPanel.tsx`.
- Status pill: dot color reuses `statusDotClass` semantics — sage-400 for `active`, stone-400 for `stale`.
- Empty state: `data-testid="memory-tree-integrations-empty"`, single line "No integrations connected" matching `MemorySources.tsx` convention.
- Scroll: `max-h-48 overflow-y-auto` once past ~5 rows so the strip never dominates the panel.
## i18n
New keys colocated with `memoryTree.status.*` in `app/src/lib/i18n/en.ts`:
| key | English |
| --- | --- |
| `memoryTree.status.integrationsTitle` | Per-integration health |
| `memoryTree.status.integrationsEmpty` | No integrations connected |
| `memoryTree.status.integrationActive` | Active |
| `memoryTree.status.integrationStale` | Stale |
| `memoryTree.status.integrationChunks` | {count} chunks |
All 13 non-English locales (`ar`, `bn`, `de`, `es`, `fr`, `hi`, `id`, `it`, `ko`, `pl`, `pt`, `ru`, `zh-CN`) get **real translations** in the same PR, per CLAUDE.md i18n rule. `pnpm i18n:check` and `pnpm i18n:english:check` must pass.
## Testing
**Vitest** (`MemoryTreeStatusPanel.test.tsx`):
1. Renders integration rows from `memory_sync_status_list` (mocked).
2. Renders empty state when list is empty.
3. Status mapping: a row with `freshness='active'` shows `Active`; `freshness='recent'` and `freshness='idle'` both show `Stale`.
4. Icon fallback for unknown provider doesn't throw.
5. Relative-time label uses `formatRelativeMs` (frozen clock).
6. Shared poll: both `memoryTreePipelineStatus` and `memorySyncStatusList` are called on the same tick (mock both, advance fake timers, assert call counts).
**Coverage:** changed-lines ≥ 80 % (CI gate). The mapping + render branches are small and trivially testable; achievable.
**No new Rust tests** — no Rust changed. Existing `memory_sync_status_list` test coverage continues to validate the wire shape.
**No new E2E spec** — covered by the existing intelligence smoke test plus the unit tests above. (E2E for a render-only sub-component is overkill.)
## Deviations from issue acceptance criteria
Will be noted explicitly in PR body so reviewers see them up front:
1. **AC #1** says `memory_tree_pipeline_status` returns an `integrations` array. We **don't** extend that RPC; we consume `memory_sync_status_list` instead. Same data, cleaner contract, no schema bump.
2. **AC #2** says status is `Active / Stale / Error`. We ship **Active / Stale** only. Per-provider `Error` requires new core work (`mem_tree_jobs` has no `source_kind` / `source_id` column today; we'd have to parse `payload_json` per row or add a column). Deferred to a follow-up issue filed alongside this PR.
Remaining ACs (list renders below status panel, empty state, polling shares parent, i18n parity, ≥80 % coverage) are met as specified.
## Risks
- **Visual crowding** if many providers are connected. Mitigated by `max-h-48 overflow-y-auto`.
- **Empty `memory_sync_status_list` when chunks haven't flowed yet** — the strip will render empty even for a freshly-installed integration. Acceptable for v1 (the issue's same gap); when per-provider error tracking lands, "configured but never produced chunks" can be its own state.
## Out of scope (filed as follow-up)
- Per-provider `Error` state. Open follow-up issue: "Per-provider error attribution for Memory Tree" — proposes either parsing `payload_json` for failed jobs to extract `source_id`, or adding a typed `source_kind` column to `mem_tree_jobs` (probably the latter, with a one-shot migration).
@@ -1,53 +0,0 @@
# Telegram (and all channels) honor `chat_provider` workload routing
**Issue:** [#3098](https://github.com/tinyhumansai/openhuman/issues/3098), sub-issue 1.
**Scope:** Sub-issue 1 only. Sub-issues 2 (Telegram file commits — by-design question), 3 (skills not running), and 4 (Brave Search) are out of scope.
## Problem
A user picks Ollama in **Settings → AI** (which writes `chat_provider = "ollama:<model>"` to config), but the Telegram bot still routes through the managed OpenHuman cloud backend and the user's `config.default_model`. The Ollama selection is silently ignored by every channel (Telegram, Discord, Slack, iMessage, Mattermost).
## Root cause
`src/openhuman/channels/runtime/startup.rs:171-183, 267-270, 682-695` builds the channels runtime provider via `create_intelligent_routing_provider(…)` — an exclusively cloud-backed chain (`OpenHumanBackendProvider` wrapped in `ReliableProvider` and the legacy hint-based `IntelligentRoutingProvider`). It pairs that provider with `ctx.model = config.default_model.unwrap_or(DEFAULT_MODEL)`.
It never consults `provider_for_role("chat", &config)` or `Config::workload_local_model("chat")` — the documented single source of truth for "is this workload local?" (`src/openhuman/config/schema/types.rs:511-544`). The unified workload factory `inference::provider::create_chat_provider` (`src/openhuman/inference/provider/factory.rs:206-217`) already knows how to build the right `(provider, model_id)` for any chat-workload string (`"ollama:…"`, `"lmstudio:…"`, `"<byok-slug>:…"`, `"openhuman"`, `"cloud"`). The channel runtime simply doesn't use it.
## Fix
In `runtime/startup.rs`, branch on `provider_for_role("chat", &config)` once during runtime setup:
- **Workload override path** (`chat_provider` is `"ollama:…"`, `"lmstudio:…"`, `"<byok-slug>:…"`, or `"claude_agent_sdk[:…]"`): build the provider via `inference::provider::create_chat_provider("chat", &config)`. Use the returned `model_id` as `ctx.model`. The cache key (`ctx.default_provider`) becomes the slug portion of the provider string (e.g. `"ollama"`).
- **Default path** (unset, `"cloud"`, or `"openhuman"`): keep the existing `create_intelligent_routing_provider` chain verbatim. `ctx.model` continues to come from `config.default_model`. Zero behavior change for cloud users.
The branch happens once during channel-runtime startup. All channels share `ChannelRuntimeContext`, so the fix uniformly covers Telegram, Discord, Slack, iMessage, Mattermost (and Matrix when feature-gated on).
## Behavior matrix
| `chat_provider` | Today | After fix |
|---|---|---|
| unset / `"cloud"` / `"openhuman"` | Cloud + `default_model` | Cloud + `default_model` (unchanged) |
| `"ollama:llama3.2"` | Cloud + `default_model` (bug) | Ollama + `llama3.2` |
| `"openai:gpt-4o"` (BYOK) | Cloud + `default_model` (bug) | OpenAI + `gpt-4o` |
## Tests
Unit-level coverage in the existing channels runtime test surface:
1. `chat_provider` unset → `ctx.model == config.default_model` (regression guard for cloud users).
2. `chat_provider = "ollama:llama3.2"``ctx.model == "llama3.2"` and `ctx.default_provider == "ollama"`.
3. `chat_provider = "cloud"` → identical to (1).
Tests use the existing `test_support.rs` harness and `Config` builders. No new mocks required; the workload factory is already exercised by `factory_tests.rs`.
## Non-goals (deliberate)
- **`/model <id>` per-conversation override** — `routes.rs:169-211`'s `get_or_create_provider` ignores the provider name and always rebuilds a cloud provider. This is a pre-existing limitation unrelated to this fix; addressing it would expand scope significantly. Once the default is correct, the `/model` command becomes a model-name-only override against whichever provider the channel runtime was constructed with, which is a reasonable interim state.
- Sub-issues 2, 3, 4 of #3098 — separate root causes, will each get their own PR if/when triaged.
- Any change to `local_ai.usage.*` (the deprecated legacy hint-based routing) — explicitly out of scope per the comment in `config/schema/types.rs:520-523`.
## Blast radius
- One file edited: `src/openhuman/channels/runtime/startup.rs` (~15 lines changed).
- Cloud-only users: zero behavior change (the default branch is the existing code path).
- Local-model users: gain a working Telegram/Discord/Slack/etc. experience with their selected Ollama (or BYOK) provider.
@@ -1,349 +0,0 @@
# AgentBox Marketplace Integration
**Issue:** [tinyhumansai/openhuman#3620](https://github.com/tinyhumansai/openhuman/issues/3620)
**Status:** Draft — design approved, plan pending
**Date:** 2026-06-12
## Background
OpenHuman has been onboarded to the **AgentBox POC** on GMI Cloud — an agent marketplace where containerized agents are registered, deployed, and distributed. The platform invokes containers over HTTP with a polling-based long-running contract and injects an OpenAI-compatible LLM (GMI MaaS) at runtime.
This design covers the code-side work to make `openhuman-core` a valid AgentBox container. Operational tasks (console registration, image push, deployment, API-key handling in a secrets manager, end-to-end marketplace validation) are out of scope of this document and remain the operator's responsibility.
## AgentBox contract (as documented)
Verified from `https://docs.gmicloud.ai/agentbox-marketplace/`:
- `POST /run`
- Request: `{ "payload": <agent-defined> }`
- Response: `202 { "job_id": "<uuid>" }`
- `GET /jobs/{job_id}`
- Response: `200 { "status": "pending" | "running" | "completed" | "failed", "result": {...}, "error": "..." }`
- `404` for unknown ids.
- Pattern is **polling-only**. No SSE / WebSockets / chunked streaming.
- Platform-injected env vars at runtime:
- `GMI_MAAS_BASE_URL` — OpenAI-compatible base URL.
- `GMI_MAAS_API_KEY` — MaaS API key (must remain unset in image).
- `GMI_MODELS` — model id to call.
- Container image source: any public/private Docker registry.
- Listening port, healthcheck endpoint, and `payload` shape are agent-defined.
## Goals
1. Stand up `POST /run` and `GET /jobs/{job_id}` in `openhuman-core`, conforming to the AgentBox contract.
2. Drive each `/run` invocation through the **full agent runtime** (skills, tools, memory) — the same path chat uses.
3. Plumb `GMI_MAAS_*` env vars into the inference provider catalog so the agent actually uses GMI MaaS at runtime.
4. Keep the change zero-impact on the desktop build (routes off by default).
## Non-goals
- Persistent job store. Jobs are kept in-memory; lost on container restart.
- Streaming responses. AgentBox's contract is polling-only.
- Marketplace registration / image push / API-key storage / e2e marketplace validation — operator tasks.
- A separate Dockerfile. The existing one is reused with new env defaults.
- Backwards-compatibility shims — `OPENHUMAN_AGENTBOX_MODE` is new; no migration needed.
## Architecture
A new domain `src/openhuman/agentbox/` with the canonical module shape:
| File | Role |
| -------------- | ----------------------------------------------------------------------------------------------------------------- |
| `mod.rs` | Export-only: `pub mod` + `pub use`. No business logic. |
| `types.rs` | `RunRequest`, `RunResponse`, `JobStatus`, `JobRecord`, `RunPayload`, `RunResult`. |
| `store.rs` | `JobStore` wrapping `DashMap<JobId, JobRecord>` behind `Arc`. Insert / get / update / sweep evicted terminal jobs.|
| `ops.rs` | `submit_run`, `get_job`, `run_job` worker, sweep task. |
| `http.rs` | Axum sub-router with `POST /run` and `GET /jobs/{job_id}`. Handler functions delegate to `ops.rs`. |
| `env.rs` | `register_gmi_provider_if_present()` — reads `GMI_MAAS_*` at startup and wires the OpenAI-compatible provider. |
| `http_tests.rs`| Axum `TestServer` integration tests. |
The router is mounted in `src/core/jsonrpc.rs::build_core_http_router` only when `OPENHUMAN_AGENTBOX_MODE=1`. Default is off, so desktop builds are unaffected.
### Why a new domain
Per `AGENTS.md`: "New functionality → dedicated subdirectory." AgentBox is a transport surface specific to GMI Cloud's marketplace contract. It is not a generic concern of the core HTTP server, so it does not belong in `src/core/`. It is not a domain-of-substance like `agent` or `threads`; it is an adapter layer that delegates inward.
### Mounting point
In `build_core_http_router`:
```rust
let router = Router::new()
.route("/", get(root_handler))
.route("/health", get(health_handler))
// ... existing routes ...
.nest("/v1", crate::openhuman::inference::http::router());
let router = if std::env::var("OPENHUMAN_AGENTBOX_MODE").as_deref() == Ok("1") {
router.merge(crate::openhuman::agentbox::http::router(job_store.clone()))
} else {
router
};
```
The `JobStore` is created once at startup in `run_server_embedded` and shared with both the AgentBox router and the sweep task.
## HTTP contract
### `POST /run`
Request body:
```json
{ "payload": { "message": "<string>", "thread_id": "<string?>" } }
```
`thread_id` is optional. When omitted, a new thread is created for the job. When supplied, the job runs in the existing thread (allowing multi-turn flows if the marketplace consumer threads state).
Successful response — `202 Accepted`:
```json
{ "job_id": "<uuid>" }
```
Error responses:
- `400 Bad Request``payload` missing, `message` missing or empty, or body not valid JSON. Body: `{ "error": "<reason>" }`.
### `GET /jobs/{job_id}`
Successful response — `200 OK`:
```json
{
"status": "pending" | "running" | "completed" | "failed",
"result": { "message": "<assistant reply>", "thread_id": "<string>" },
"error": "<string>"
}
```
`result` is present only when `status == "completed"`. `error` is present only when `status == "failed"`. Field order matches AgentBox's documented response shape.
Error responses:
- `404 Not Found` — unknown or evicted job id. Body: `{ "error": "job not found" }`.
### Authentication
Both routes are **unauthenticated at the container boundary** — AgentBox's edge handles auth before reaching us. They are added to the public path list in `src/core/auth.rs` so `rpc_auth_middleware` skips them.
The existing `/rpc` route stays bearer-protected. In AgentBox deployments the container binds to `0.0.0.0`, so the bearer-protected routes are reachable on the network — that is acceptable because the bearer is generated per-launch and never leaves the container's runtime memory (no env-var exposure). For added defense, AgentBox mode logs a warning at startup reminding the operator that only `/run`, `/jobs/*`, and `/health` are intended to be public.
### Healthcheck
Reuse existing `GET /health`. No new contract.
### Routes summary
| Path | Method | Auth | Added by |
| ---------------- | ------ | -------- | ------------- |
| `/health` | GET | none | existing |
| `/run` | POST | none | this design |
| `/jobs/{job_id}` | GET | none | this design |
| `/rpc` | POST | bearer | existing |
| `/v1/*` | * | (existing) | existing |
## Job execution
### Worker flow
`run_job(store, job_id, RunPayload { message, thread_id })`:
1. Update job status `pending → running` in the store.
2. Resolve thread: if `thread_id` supplied, fetch via `threads::ops`; otherwise create a new thread via existing thread-creation ops. Capture the thread id for the result.
3. Invoke the agent dispatcher with the user message. This goes through the **full agent runtime** — same entrypoint as chat: skills, tools, memory, prompt injection guard, the lot. Reuses the existing dispatcher's public submission API.
4. Await dispatcher completion. Capture the final assistant turn's text content.
5. Write `JobRecord { status: Completed, result: Some(RunResult { message: assistant_text, thread_id }), error: None }` to the store.
6. On any error (dispatcher error, thread-resolve error, timeout): write `JobRecord { status: Failed, result: None, error: Some(err_string) }`.
### Cancellation & timeout
- Hard cap configurable via `OPENHUMAN_AGENTBOX_JOB_TIMEOUT_SECS`. Default: `600` (10 minutes).
- Wrapped in `tokio::time::timeout`. On expiry, the future is dropped; the agent dispatcher's existing tokio cancellation semantics handle cleanup.
- On timeout: `status = "failed"`, `error = "job timeout after <N>s"`.
### Job retention
- Terminal jobs (`completed` / `failed`) are retained for **1 hour** after completion, then evicted by a background sweep.
- A `tokio::spawn` background task wakes every 60 seconds and removes jobs whose `terminal_at` is older than the retention window.
- This bounds memory under sustained traffic without forcing persistence in v1.
- If a polling client misses the retention window, they get `404 job not found` — acceptable per AgentBox's contract (no docs claim indefinite retention).
### Concurrency
- The `JobStore` is `DashMap`-backed and safe for concurrent insert/get/update.
- `submit_run` always spawns the worker via `tokio::spawn` and returns immediately. There is no semaphore in v1 — the underlying agent runtime already handles concurrency at its level.
- Future: if concurrent job pressure becomes a problem, add a configurable semaphore around `tokio::spawn`. Out of scope here.
## GMI MaaS provider bridge
### Startup
`env.rs::register_gmi_provider_if_present()` runs once during `run_server_embedded` initialization, before the agent runtime accepts work.
Reads:
- `GMI_MAAS_BASE_URL` — required.
- `GMI_MAAS_API_KEY` — required.
- `GMI_MODELS` — required, single model id (AgentBox docs example: `deepseek-ai/DeepSeek-V4-Pro`).
Behavior:
- All three present → register an OpenAI-compatible cloud provider named `"gmi-maas"` into the inference provider catalog using the existing `compatible_provider_impl`. The model from `GMI_MODELS` becomes its default. Mark `"gmi-maas"` as the active provider for agent runtime invocations.
- Any missing → log a single `warn!` line listing which vars are missing, skip registration, continue startup. The core still runs (useful for local testing of the `/run` route with a stubbed inference layer), but agent calls that need an LLM will fail until a provider is configured by other means.
### Logging rules
Per repo logging conventions: log the base URL and model id at `info!` level when registered; never log the API key (not even truncated). Use the stable `[agentbox]` prefix for AgentBox-side logs and `[agentbox::gmi]` for the provider bridge.
## Data shapes
```rust
// types.rs
#[derive(Debug, Clone, Deserialize)]
pub struct RunRequest {
pub payload: RunPayload,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RunPayload {
pub message: String,
#[serde(default)]
pub thread_id: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RunResponse {
pub job_id: String,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum JobStatus {
Pending,
Running,
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize)]
pub struct RunResult {
pub message: String,
pub thread_id: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct JobView {
pub status: JobStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<RunResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct JobRecord {
pub status: JobStatus,
pub result: Option<RunResult>,
pub error: Option<String>,
pub created_at: std::time::Instant,
pub terminal_at: Option<std::time::Instant>,
}
```
`JobView` is the serialization-only projection returned by `GET /jobs/{job_id}`. `JobRecord` is the internal state.
## Configuration surface
New env vars:
| Var | Default | Role |
| ------------------------------------- | ------------ | ------------------------------------------------- |
| `OPENHUMAN_AGENTBOX_MODE` | `0` | `1` enables `/run` + `/jobs/*` routes. |
| `OPENHUMAN_AGENTBOX_JOB_TIMEOUT_SECS` | `600` | Hard cap per job invocation. |
| `GMI_MAAS_BASE_URL` | unset | OpenAI-compatible base URL (AgentBox-injected). |
| `GMI_MAAS_API_KEY` | unset | MaaS API key (AgentBox-injected at runtime). |
| `GMI_MODELS` | unset | Model id (AgentBox-injected). |
All five are documented in `.env.example` with comments tying them back to the AgentBox console wizard.
## Testing
### Unit tests (inline `#[cfg(test)] mod tests`)
- `store.rs`
- `insert` / `get` round-trip.
- `mark_completed` and `mark_failed` update status and `terminal_at`.
- `sweep` evicts terminal jobs older than the retention window; leaves running and recent terminal jobs untouched.
- `ops.rs`
- `submit_run` returns a valid v4 uuid and a job in `Pending` status.
- `run_job` happy path with a mocked dispatcher → `Completed` with the assistant message captured.
- `run_job` dispatcher-error path → `Failed` with the error string captured.
- `run_job` timeout path → `Failed` with `"job timeout..."`.
- `env.rs`
- All three vars present → provider registered, `info!` logged with base URL and model (no key).
- Any one missing → no provider registered, `warn!` logged listing missing vars.
- `GMI_MAAS_API_KEY` value never appears in captured logs.
### HTTP integration tests (`http_tests.rs`)
Uses Axum's `TestServer`:
- `POST /run` with valid body → `202` and a uuid in `job_id`.
- `POST /run` with missing `message``400` and `{ "error": ... }` body.
- `POST /run` with malformed JSON → `400`.
- `GET /jobs/{job_id}` for unknown id → `404` and `{ "error": "job not found" }`.
- End-to-end with a fast mocked dispatcher: submit `/run`, poll `/jobs/{job_id}` until `completed`, assert result shape.
- Routes are not registered when `OPENHUMAN_AGENTBOX_MODE` is unset → `POST /run` returns `404`.
### Rust integration test in `tests/`
A `tests/agentbox_e2e.rs` against the real core binary started by `scripts/test-rust-with-mock.sh`:
- Boot core with `OPENHUMAN_AGENTBOX_MODE=1` and stubbed `GMI_MAAS_*`.
- Submit a `/run` with a canned message that the mock provider answers deterministically.
- Poll until `completed`, assert the assistant reply matches.
### Out of scope for tests
- E2E against the real AgentBox marketplace. That is the operator's manual acceptance step from the issue.
## Docker
The existing `Dockerfile` is unchanged structurally. Two additions:
1. Default `ENV OPENHUMAN_AGENTBOX_MODE=0` so desktop bundles built from the same Dockerfile do not flip on the public routes.
2. `EXPOSE 7788` stays — AgentBox does not require a specific port; the value is passed through during console registration.
Operator workflow at deploy time (set in the AgentBox console wizard's "Env Variables" step):
- `OPENHUMAN_AGENTBOX_MODE=1`
- `GMI_MAAS_BASE_URL` / `GMI_MAAS_API_KEY` / `GMI_MODELS` are injected automatically by AgentBox.
- Optional: `OPENHUMAN_AGENTBOX_JOB_TIMEOUT_SECS` override.
No image rebuild needed to switch modes.
## Documentation
Add `gitbooks/developing/agentbox-deployment.md` — a short runbook covering:
1. The 4-step AgentBox console wizard, mapped to our env vars.
2. The image tag/registry conventions to use for releases destined for the marketplace.
3. The polling-only contract recap (`/run` + `/jobs/*`) for anyone debugging from the AgentBox console's test panel.
4. Timeout & retention defaults and how to tune them.
No CLAUDE.md / AGENTS.md change — AgentBox is a transport surface, not a core convention.
## Risks & open questions
- **Job-store memory growth under burst traffic.** Mitigation: 1-hour retention + 60-second sweep. If marketplace traffic shape is unknown at deploy time, an operator can shorten retention via a future `OPENHUMAN_AGENTBOX_JOB_RETENTION_SECS` env var. Out of scope for v1.
- **`thread_id` semantics across calls.** v1 trusts the caller's id verbatim. If a marketplace user supplies an id that does not resolve in this deployment's workspace, the `/run` handler still queues the job (the sync handler does not block on thread lookup), and the worker writes `status: "failed"` with `error: "thread not found"`. No cross-tenant leakage because each deployment owns its workspace.
- **Healthcheck contract.** AgentBox docs do not specify one. We expose `/health` and rely on AgentBox console configuring it. If marketplace validation requires a different path, that's a small follow-up.
- **No persistence on restart.** First deployment takes 1025 minutes per the issue, so restarts are rare — acceptable trade-off. A later PR can swap the in-memory store for a SQLite-backed one without touching the HTTP layer.
- **Approval gate interaction.** The existing approval gate parks interactive chat turns; AgentBox jobs are "background/cron" by nature and should pass through. The worker submission tags the request as background origin so the gate does not park it. To confirm during implementation.
## Acceptance criteria for this PR (code side only)
- `cargo check` and `pnpm rust:check` pass.
- New `src/openhuman/agentbox/` domain compiles and is wired into `build_core_http_router` behind `OPENHUMAN_AGENTBOX_MODE=1`.
- `POST /run` and `GET /jobs/{job_id}` match the contract in this doc (verified by `http_tests.rs`).
- `GMI_MAAS_*` env vars register the OpenAI-compatible provider at startup.
- All new code paths logged with `[agentbox]` / `[agentbox::gmi]` prefixes; secrets never logged.
- Unit + integration test suites pass with `pnpm test:rust`.
- `.env.example` updated.
- `gitbooks/developing/agentbox-deployment.md` added.
- Coverage gate (≥80% on changed lines) satisfied.
Operational acceptance criteria from the issue (marketplace listing visible, container deploys, agent responds, API key stored securely) are explicitly **out of scope of this PR** and remain the operator's checklist.
@@ -1,94 +0,0 @@
# 00 — Baseline: crate, features, native links
Current status (2026-07-03): baseline dependency alignment is complete in both
Cargo worlds. `tinyagents 1.5.0` is resolved with the `sqlite` feature,
OpenHuman pins `rusqlite = "=0.40.0"`, both worlds patch through
`vendor/rusqlite-0.40.0` and `vendor/libsqlite3-sys-0.38.0`, and the SDK-gaps
inventory has been refreshed against the published 1.3.0 crate source.
## Steps
1. **Bump `tinyagents` to `"1.5.0"`** (done in both Cargo worlds — root and
`app/src-tauri/`). Known 1.1→1.2 break already handled
(`MessageDelta::text` ctor). Note: the `openai` crate feature was removed
after 1.2.0 (1.2.1+ features are only `sqlite`/`repl`) — we never enabled
it, so no impact. See "1.3.0 delta" below for new API this plan uses.
2. **Align rusqlite to 0.40** in both worlds (`Cargo.toml` root and
`app/src-tauri/Cargo.toml`). OpenHuman pins `rusqlite = "=0.40.0"` and
enables `tinyagents = { version = "1.5.0", features = ["sqlite"] }`.
Compatibility notes:
- `rusqlite 0.40` and `libsqlite3-sys 0.38` are consumed directly from
crates.io. Their build scripts use the `cfg_select!` macro (stable from
Rust 1.96 — `rust-toolchain.toml` pins `1.96.1`), so the earlier vendored
copies under `vendor/` that backported `cfg_select!` to `#[cfg]` for the
old 1.93 toolchain have been deleted.
- The `channel-matrix` feature (and its `matrix-sdk` dependency) was dropped,
which removes `matrix-sdk-sqlite` from the tree entirely. That crate pinned
`rusqlite 0.37` and previously had to be vendored/patched onto the `0.40`
line to avoid a second native sqlite chain; with Matrix gone the patch is
deleted.
- `whatsapp-rust/sqlite-storage` is disabled because its Diesel storage
links sqlite independently; `whatsapp-web` temporarily uses
`wacore::store::InMemoryBackend` and logs the non-durable session mode.
3. **Unlocks:** crate `SqliteCheckpointer` → later deletion of
`src/openhuman/tinyagents/checkpoint.rs` (`SqlRunLedgerCheckpointer`,
250 lines) once graphs are re-pointed and `graph_checkpoints` rows are
migrated or expired (see `04-sessions/`).
4. **Do NOT enable `openai` feature** — OpenHuman providers stay the product
source of truth for credentials/billing (spec non-goal).
5. Update the "TinyAgents crate: features & compatibility" section in
`gitbooks/developing/architecture/agent-harness.md` and the Cargo.toml
comment (lines 4655) after the flip.
6. Mark `docs/tinyagents-sdk-gaps.md` items 1, 2, 3, 4, 7, 10, 11, 12
as shipped in 1.2.01.3.0 (verified against crate source); keep only the
residuals, re-verified against 1.3.0: no free-form `ToolSchema` metadata
map, no reasoning field on the middleware-facing `harness::model::ModelDelta`,
no `root_run_id` on `RunConfig`, no USD field on `Usage`.
## 1.3.0 delta (verified from the published crate source)
Same `rusqlite ^0.40 bundled` pin and feature set as 1.2.1. OpenHuman currently
pins the compatible patch release locally. New API this plan's workstreams
should use directly:
- `AgentEvent::ToolsFiltered { by, excluded, remaining }` — exposure decisions
are now event-native (01.3).
- `AgentEvent::{BudgetReserved, BudgetReconciled}` + `BudgetLimits.
max_cached_input_tokens` + reservation tracking — pre-spend
reserve/reconcile (06).
- `AgentEvent::ControlApplied` + `MiddlewareControl::kind()/precedence()` —
typed middleware control outcomes with defined precedence (sdk-gaps §13
closed).
- `ContextualToolSelectionMiddleware::inheriting(...)` — parent→child
narrowing composition built in (01.3, 07).
- `ToolPolicyMiddleware::{require_sandbox, require_approval,
enforce_result_bytes}` builders (01.1).
- `ParallelOptions::{with_item_timeout, with_total_timeout,
with_cancellation}` (08.1/08.2).
- `graph::testkit` TaskStore conformance contracts
(`taskstore_concurrent_contract`, `taskstore_replay_contract`) (07.2, 11).
- `WorkspaceDescriptor::enforce(path, events)` — violation check that also
emits events (08.5).
- `ModelSelection.allow_retired`; OpenAI-compat runtime model listing
(`ModelListing`/`ModelListWire`) for catalog discovery (02.1/02.4).
- Registry: `ComponentKind::{Middleware, Checkpointer, TaskStore, Listener}`,
alias diagnostics (`AliasBinding`, cross-kind name-reuse) (10).
- `EventRecord::with_stream_id` — stream ids on event records (05.1).
## Acceptance
- Both Cargo worlds `cargo check` clean with `sqlite` feature on:
`cargo check --manifest-path Cargo.toml`,
`cargo check --manifest-path Cargo.toml --all-features`, and
`cargo check --manifest-path app/src-tauri/Cargo.toml`.
- One duplicate-free `cargo tree -i libsqlite3-sys` per world, rooted at
`vendor/libsqlite3-sys-0.38.0`.
- Docs updated; sdk-gaps marked.
Verified commands:
- `cargo check --manifest-path Cargo.toml --message-format=short`
- `cargo check --manifest-path Cargo.toml --all-features --message-format=short`
- `cargo check --manifest-path app/src-tauri/Cargo.toml --message-format=short`
- `cargo tree --manifest-path Cargo.toml --all-features -i libsqlite3-sys`
- `cargo tree --manifest-path app/src-tauri/Cargo.toml --all-features -i libsqlite3-sys`
@@ -1,62 +0,0 @@
# 01.1 — ToolPolicy round-trip
The spec's "SDK gap: ToolSchema has no metadata map" is obsolete: the crate
`Tool` trait has `policy() -> ToolPolicy` and `ToolRegistry::policies()`.
Current status (2026-07-02): adapters expose classified SDK policies, the
crate `ToolPolicyMiddleware` is installed from `harness.tools().policies()`, and
`ToolOutputMiddleware` now reads per-tool result caps from that registry
snapshot instead of rebuilding a `name -> Arc<dyn Tool>` lookup. The remaining
crate-internal OpenHuman lookups are behavior-preserving overlays for data the
crate policy snapshot cannot represent yet: args-aware external effects,
CLI/RPC-only scope, args-aware permission level, and generated-tool runtime
context. The local `tinyagents/middleware.rs` module is crate-internal; turn
config types stay crate-visible only through `openhuman::tinyagents` re-exports.
The `ToolAdapter`/`SharedToolAdapter` execution wrappers are crate-internal
implementation details of the shared runner.
## Steps
1. In `src/openhuman/tinyagents/tools.rs` (`ToolAdapter`/`SharedToolAdapter`),
implement `policy()` by mapping OpenHuman trait methods
(`src/openhuman/tools/traits.rs`): permission level → `ToolAccess
{ approval_required, background_safe }`; `external_effect`
`ToolSideEffects`; timeout policy → `ToolRuntime.timeout_ms`;
concurrency safety → `ToolRuntime.idempotent`; `max_result_size_chars`
`ToolRuntime.max_result_bytes`; sandbox mode → `SandboxMode`.
Every adapter-produced policy sets `classified: true`.
2. Partially done: rework `ApprovalSecurityMiddleware`, `CliRpcOnlyMiddleware`,
`ToolPolicyMiddleware`, and `ToolOutputMiddleware`
(`src/openhuman/tinyagents/middleware.rs`) to read `ToolPolicy` from the
registry instead of the shared `name → Arc<dyn Tool>` side-lookup.
`ToolOutputMiddleware` is registry-backed. Keep the crate-internal OpenHuman
overlays the static crate policy cannot yet express: `external_effect_with_args`,
`ToolScope::CliRpcOnly`, `permission_level_with_args`, and
`generated_runtime_context`.
3. Partially done: install crate `ToolPolicyMiddleware` in
`assemble_turn_harness` (`src/openhuman/tinyagents/mod.rs`) with
`require_sandbox(true)` only. TinyAgents 1.3 exposes classification,
approval, and result-byte gates, but OpenHuman still keeps args-aware
approval/audit and legacy result-cap wording in local overlays. Enable the
stricter crate gates only after those overlays have equivalent policy
coverage or can be deleted. Assert every registered tool is classified in the
adapter-inventory test when those gates become fail-closed.
4. Done for output budgeting: `TurnContextMiddleware.install` takes the SDK
policy snapshot instead of the `&tool_sets` parameter.
## Deletions
- Deleted: `ToolOutputMiddleware`'s `name → Arc<dyn Tool>` policy snapshot in
`tinyagents/middleware.rs`.
- Remaining as crate-internal overlays by design until the SDK policy surface
grows equivalent metadata: `external_effect_with_args`, `ToolScope::CliRpcOnly`,
`permission_level_with_args`, and `generated_runtime_context` overlays.
- Deleted: redundant per-call trait re-queries in the legacy
`agent_tool_exec.rs` policy chain.
## Acceptance
- `ToolRegistry::policies()` snapshot test: all tools classified, policies
serialize stably.
- Approval/security/output behavior parity tests still green
(middleware suite ~22 tests).
@@ -1,34 +0,0 @@
# 01.2 — Unknown-tool recovery via RunPolicy
Crate `RunPolicy.unknown_tool` now has `UnknownToolPolicy::{Fail,
ReturnToolError, Rewrite { tool_name }}` plus an `AgentEvent::UnknownToolCall`
variant — the sentinel is deletable.
Current status (2026-07-02): `run_policy_for` sets
`UnknownToolPolicy::ReturnToolError`, `OpenhumanEventBridge` projects
`AgentEvent::UnknownToolCall` into grep-friendly diagnostics, and the old
sentinel/middleware symbols are gone from source.
## Steps
1. Done: `run_policy_for` sets `RunPolicy.unknown_tool =
UnknownToolPolicy::ReturnToolError`, preserving the legacy "model corrects
itself" behavior with the original requested name).
2. Done: no wording-only sentinel shim remains in `tinyagents/middleware.rs`.
3. Done: `AgentEvent::UnknownToolCall` reaches `OpenhumanEventBridge` and is
projected distinctly from "tool executed and failed" in debug logs.
## Deletions
- Deleted: `UNKNOWN_TOOL_SENTINEL` + sentinel tool registration in
`src/openhuman/tinyagents/tools.rs`.
- Deleted: `UnknownToolRewriteMiddleware` in `src/openhuman/tinyagents/middleware.rs`
(+ its tests, rewritten as RunPolicy behavior tests).
## Acceptance
- Hallucinated tool name -> recoverable tool error, run continues; event
stream records the original requested name; no sentinel appears in
transcripts or advertised schemas.
- Sub-agent and top-level tests accept TinyAgents' fixed recoverable-tool-error
wording while preserving the original requested tool name in events/logs.
@@ -1,67 +0,0 @@
# 01.3 — Dynamic tool exposure as middleware
Replace scattered pre-filtering with crate selection middleware.
Current OpenHuman surfaces: `agent/harness/tool_filter.rs` (mechanics),
`tools/user_filter.rs`, sub-agent `tool_prep.rs`, dynamic delegation refresh in
`session/turn/tools.rs`, channel permission ceiling checks.
Current status: the shared TinyAgents runner still registers only the
OpenHuman-computed callable tools, preserving hidden-tool execution semantics.
It now tags OpenHuman run contexts and emits a diagnostics-only
`AgentEvent::ToolsFiltered` via `openhuman_tool_visibility` when that existing
allowlist withholds candidate tools. Full middleware-owned selection remains.
Production tool exposure now goes exclusively through
`run_turn_via_tinyagents_shared` + `SharedToolAdapter`; the old non-shared
`ToolAdapter` wrapper is test-only.
`agent/harness/tool_filter.rs` is still live for `integrations_agent` toolkit
spawns: the runner uses it to choose a compact Composio action set before
registering dynamic tools.
`src/openhuman/agent/harness/subagent_runner/tool_prep.rs` is also still live,
and it currently mixes filtering (`filter_tool_indices`, nested-delegation
stripping, denylist checks, toolkit top-K budgets) with non-filter helpers
(`load_prompt_source`, text-mode protocol instructions). Delete it only after
`ContextualToolSelectionMiddleware` owns child/toolkit selection and those
non-filter helpers have moved to narrower modules. The `tool_prep` helper
surface has been narrowed to `subagent_runner` except for the text-mode protocol
renderer used by prompt debug dumps. The turn-local parent-context/progress and
cached integration refresh helpers in `session/turn/tools.rs` are scoped to the
turn module, while the cross-surface integration fetch and delegation refresh
entrypoints remain live.
## Steps
1. Express agent `tool_allowlist`/`tool_denylist`, sub-agent tool scope,
MCP tool visibility, and the channel permission ceiling as a composed
`ToolAllowlistMiddleware` + one OpenHuman
`ContextualToolSelectionMiddleware`. TinyAgents 1.3.0's selection context
carries run id, depth, tags, and requested model; OpenHuman-specific agent
id, task kind, security tier, and channel either stay in local middleware
state or are encoded into tags. Inheritance rule: children can only narrow —
use `ContextualToolSelectionMiddleware::inheriting(...)`.
2. Fail closed when policy metadata is missing (unclassified → not exposed);
exposure decisions are event-native via `AgentEvent::ToolsFiltered
{ by, excluded, remaining }` (1.3.0) — projected into the bridge as
structured diagnostics today.
3. Move the visible-set computation, dynamic delegation refresh, and skill-event
catalogue reconciliation out of
`src/openhuman/agent/harness/session/turn/tools.rs` (616 lines) plus
`subagent_runner/tool_prep.rs` (344 lines) into the middleware; the turn code
only declares candidate tool sets and parent execution context.
4. Keep product policy sources (registry definitions, security tier tables)
in OpenHuman — the middleware consumes them.
## Deletions
- `agent/harness/tool_filter.rs` mechanics (299 lines; keep policy tables if
any inline).
- `subagent_runner/tool_prep.rs` (344 lines).
- Filtering/refresh blocks in `session/turn/tools.rs` (file shrinks to
parent-context and assembly glue).
## Acceptance
- Sub-agents can never see a tool the parent couldn't grant (test).
- Exposure decision visible in run events.
- CLI/RPC-only denial (`CliRpcOnlyMiddleware`) unaffected or folded in.
@@ -1,52 +0,0 @@
# 01.4 — Tool output budgets, summarizer, artifacts
Finish moving tool-result post-processing to `after_tool` middleware and
delete the legacy hooks.
## Steps
Current status: OpenHuman's local `ToolOutputMiddleware` now runs on the
TinyAgents harness as `after_tool` for payload summarization, TokenJuice
`compact_output_with_policy`, per-tool policy-derived output caps, generic byte
budgets, and action-workspace artifact spill for oversized session tool
results. The SDK's closest built-in cap is `ToolPolicyMiddleware` result-byte
enforcement, which remains disabled here because OpenHuman still owns the
legacy marker/artifact behavior. Payload summarization and TokenJuice shrinks
now emit TinyAgents `AgentEvent::Compressed`, and persisted action-workspace
artifacts are indexed in `RunContext.stores` under
`openhuman_tool_result_artifacts` while the existing `.txt` file and `file_read`
envelope remain the source of truth. The live TinyAgents middleware already
calls the parent-context-aware `PayloadSummarizer::maybe_summarize_in_parent`,
and that implementation dispatches the summarizer through
`SubAgent::invoke_in_parent` so child lineage/events inherit the parent
TinyAgents context. The older direct-executor payload-summarizer hook is
removed, the old `session/agent_tool_exec.rs` test-only parity shim is deleted,
and the stale default-Full `tokenjuice::compact_tool_output` wrapper is removed.
1. `payload_summarizer.rs` (live in `src/openhuman/tinyagents/`; oversized-result
compression via a `summarizer` sub-agent + circuit breaker):
`ToolOutputMiddleware` now calls the parent-context-aware summarizer seam,
and that live path uses `SubAgent::invoke_in_parent` for child depth/event
lineage. Remaining work: emit any additional `SummaryRecord`-style
provenance needed beyond `AgentEvent::Compressed`.
2. `tokenjuice::compact_tool_output`: deleted after confirming
`compact_output_with_policy` covers the live TinyAgents middleware and
legacy direct executor paths; decision recorded in
`src/openhuman/tokenjuice/README.md`.
3. `harness/tool_result_artifacts/mod.rs` (588 lines, artifact spill):
keep spill policy and action-workspace `.txt` writes in OpenHuman, but keep
the run's `StoreRegistry` (`RunContext.stores`) populated with structured
artifact metadata so replay can find the model-facing preview's full body.
4. Done: `session/agent_tool_exec.rs` was deleted after the live turn path moved
to TinyAgents middleware.
## Deletions
- Deleted: `session/agent_tool_exec.rs` legacy direct-executor shim.
- `tokenjuice::compact_tool_output`.
## Acceptance
- Oversized tool result → compressed with provenance event; parity fixture
vs old summarizer output shape.
- No tool-result post-processing outside middleware.
@@ -1,33 +0,0 @@
# 01 — Tooling
Move tool metadata, policy enforcement, unknown-tool recovery, dynamic
exposure, and output budgeting onto SDK primitives; delete the OpenHuman
side-lookup pattern and legacy tool plumbing.
Target SDK surface (available across tinyagents 1.2.x1.3.0; current repo lock
is 1.5.0):
- `Tool::policy() -> ToolPolicy { side_effects, runtime, access }`
serializable safety metadata (read_only/writes_files/network/destructive/
payment; timeout/retries/idempotent/sandbox/max_result_bytes; workspace
access/trusted_roots/credentials/approval_required/background_safe).
- `ToolPolicyMiddleware` (fail-closed on unclassified), `ToolAllowlistMiddleware`,
`DynamicToolSelectionMiddleware`, `ContextualToolSelectionMiddleware`,
`HumanApprovalMiddleware`.
- `RunPolicy.unknown_tool: UnknownToolPolicy::{Fail, ReturnToolError, Rewrite}`.
- `ToolRegistry::policies()`, `ToolExecutionContext.workspace:
WorkspaceDescriptor`.
Steps:
1. `01-tool-policy.md` — implement `policy()` on the adapters; retire the
`name → Arc<dyn Tool>` side-lookup.
2. `02-unknown-tool.md` — replace the sentinel with `UnknownToolPolicy`.
3. `03-dynamic-exposure.md` — allowlists/denylists/channel ceiling as
selection middleware.
4. `04-tool-output.md` — output budgets + payload summarizer as `after_tool`;
delete legacy files.
Done when: no OpenHuman middleware queries tool trait methods ad hoc; the
sentinel is gone; tool visibility decisions are middleware-owned and
event-visible; deletions in `99-deletion-ledger.md` §tooling are done.
@@ -1,35 +0,0 @@
# 02.1 — Workload routes as ModelRegistry entries
Today `run_turn_via_tinyagents_shared` registers exactly one crate-internal
`ProviderModel` per run. Target: register the run's full resolved route set so fallback and
capability resolution happen inside the SDK.
## Steps
1. Extend `assemble_turn_harness` to register one `ProviderModel` per
workload route the turn may use (`chat`, `agentic`, `reasoning`, `coding`,
`memory`, `subconscious`, `burst`, `summarization`, `vision` — from
`provider/router.rs` tier names), each
with its real `ModelProfile` (already built at construction; add
structured-output/reasoning flags from provider capabilities as they
gain accessors).
2. Use `ModelRequest::with_required_capabilities` /
`ModelSelection`/`ModelHint` so per-call needs (vision, tools, reasoning)
reject unfit models pre-dispatch instead of failing at the provider.
3. `router.rs` keeps owning tier-name → provider-string policy; the
translation to registry entries lives in one adapter fn
(`tinyagents/model.rs` or a new `tinyagents/routes.rs`).
4. Record resolution in events (`ResolvedModel`/`ModelResolutionSource`
already on `ModelResponse`).
## Deletions
- Ad hoc model-capability checks scattered on turn paths (vision gates in
`subagent_runner/ops/graph.rs`, `model_vision` param threading) once
`CapabilitySet` covers them.
## Acceptance
- A turn requesting vision on a non-vision model fails pre-dispatch with a
typed error; fallback picks the next capable route.
- Adapter-inventory test asserts the registered route set.
@@ -1,48 +0,0 @@
# 02.2 — Retry/fallback ownership → RunPolicy
`provider/reliable.rs` (1215 lines + 1443 tests) wraps several production
provider paths in retry/fallback. The crate loop is currently pinned to a single
attempt (`RunPolicy.retry.max_attempts = 1`) to avoid double-retry while
OpenHuman owns reliability. Audit then collapse to one owner: the SDK.
Current status (2026-07-02): do not delete `ReliableProvider` yet. Provider
factory paths still wrap the OpenHuman backend in `ReliableProvider` for
configured retries and model fallbacks, and `run_policy_for` explicitly sets
`policy.retry.max_attempts = 1` so TinyAgents does not double-retry while that
wrapper owns reliability. Deletion is blocked on moving the configured fallback
chain into registered crate model routes plus event-visible retry/fallback
parity. TinyAgents 1.3.0 `RunPolicy::fallback` can retry fallback model routes,
but it does not by itself emit the OpenHuman `FallbackSelected` parity signal;
that event parity requires `ModelFallbackMiddleware` or an equivalent bridge.
## Steps
1. Map `ReliableProvider` behaviors onto crate primitives:
transient 429/5xx → `RetryPolicy` (exp backoff) + `RateLimitMiddleware`;
provider chain → `FallbackPolicy` (ordered model names, needs 02.1
multi-registration); permanent config rejection / billing exhaustion →
non-retryable `ProviderError { retryable: false }` mapping in
`ProviderModel`.
2. Keep OpenHuman's error classification (`ops/http_error.rs`,
`billing_error`, `auth_error_registry`) as the mapper that fills
`ProviderError`; it is product knowledge.
3. Un-wrap `ReliableProvider` from `session/builder/factory.rs` (re-layered
there 2026-06-30) once parity tests cover: transient retry count, retry
events (`RetryScheduled`), fallback events (`FallbackSelected`), and NO
double-retry (assert total attempt counts with `MockModel::call_count`).
Keep `RunPolicy.retry.max_attempts = 1` until this swap happens; raising it
earlier would reintroduce double-retry on top of `ReliableProvider`.
4. Non-turn callers of `ReliableProvider` and its classifier helpers (memory-tree
local summarizer, memory scoring, triage error classification): either route
them through a minimal harness invoke or a small shared retry/classification
utility — inventory call sites first; do not leave a fork.
## Deletions
- `src/openhuman/inference/provider/reliable.rs` + tests (rewrite the
behaviorally-relevant cases against RunPolicy).
## Acceptance
- Attempt-count parity matrix (429, 500, config rejection, billing) green.
- `FallbackSelected`/`RetryScheduled` visible in the event bridge.
@@ -1,60 +0,0 @@
# 02.3 — Native reasoning + tool-arg streaming
tinyagents 1.3.0 `MessageDelta` has a `reasoning` field and
`StreamAccumulator::reasoning()`; `ModelStreamItem::ToolCallDelta` streams
tool-arg fragments. The out-of-band `ThinkingForwarder` is not one-shot
deletable: streaming reasoning can ride the crate stream, but OpenHuman still
uses the forwarder for non-streaming post-hoc reasoning and tool-argument
progress that preserves the UI's `tool_name`/start-event contract.
Current status (2026-07-02): streaming provider `ThinkingDelta` values are
mapped to `MessageDelta::reasoning(...)`, and `OpenhumanEventBridge` projects
`delta.reasoning` into the same parent/sub-agent thinking progress events as the
old forwarder path. Tool-call **argument** fragments now ride the crate stream
too: `forward_delta` maps each `ProviderDelta::ToolCallArgsDelta` onto a
`ModelStreamItem::ToolCallDelta(ToolDelta { call_id, content })`, the crate
agent-loop surfaces it as `MessageDelta.tool_call`, and `OpenhumanEventBridge`
projects it into `AgentProgress::ToolCallArgsDelta`. Because the crate
`ToolDelta` has only `call_id`/`content` (no `tool_name`), the tool-call
**start** event — the empty-delta `ToolCallArgsDelta` that carries the tool name
and opens the UI timeline row — still rides `ThinkingForwarder.note_tool_call`,
which records the name into a `call_id → tool_name` map (`ToolNameMap`) *shared*
with the bridge so the streamed fragments stay labelled. The forwarder remains
live for that start marker and for non-streaming post-hoc reasoning; its
`emit_tool_args` argument-fragment path has been removed. The `StreamAccumulator`
treats the terminal `Completed` response as authoritative, so streaming the
fragments never disturbs the final native tool calls.
## Steps
1. Done for streaming providers: `src/openhuman/tinyagents/model.rs` maps
provider thinking deltas to `MessageDelta::reasoning(...)` on the crate
stream, and `OpenhumanEventBridge` (`observability.rs`) projects
`delta.reasoning` into OpenHuman progress events.
2. Done: provider tool-call argument fragments map onto
`ModelStreamItem::ToolCallDelta` (crate `ToolDelta { call_id, content }`) and
are projected by `OpenhumanEventBridge`. `ToolDelta` carries no `tool_name`,
so the `tool_name`/start-event half of the UI timeline contract stays on
`ThinkingForwarder.note_tool_call`, which shares a `call_id → tool_name` map
(`ToolNameMap`) with the bridge so the streamed fragments keep their label.
Only `ThinkingForwarder.emit_tool_args` was removed.
3. Verify sub-agent child thinking deltas still reach the scope-aware bridge
(parity with the current forwarder behavior).
## Deletions
- Reasoning side of `ThinkingForwarder`: moved for streaming providers; keep the
non-streaming fallback until the non-streaming path has an equivalent event.
- Argument-fragment side of `ThinkingForwarder` (`emit_tool_args`, in
`tinyagents/model.rs`): removed — fragments ride `ToolCallDelta` and are
projected by the bridge. The tool-call **start** marker
(`note_tool_call`, empty-delta `ToolCallArgsDelta` with `tool_name`) stays,
since the crate `ToolDelta` cannot carry a tool name; it will only move if a
future crate revision adds `tool_name`/start semantics to `ToolDelta`.
## Acceptance
- UI receives visible-text, reasoning, and tool-arg deltas from crate stream
items alone, including `tool_name`/start-event semantics; streaming e2e (chat
- sub-agent) green.
- Non-streaming providers emit post-hoc reasoning once.
@@ -1,51 +0,0 @@
# 02.4 — One model catalog
Model metadata is currently split across several sources: `cost/catalog.rs`
(pricing + windows), `Config::model_registry` seeding/enrichment,
`model_context.rs` tier/pattern/local fallbacks, provider capability accessors,
provider `effective_context_window`, and `docs/inference-provider-catalog.md`.
Crate `registry::ModelCatalog` is the normalized shape
(`ModelCatalogEntry { provider, model_id, aliases, max_input_tokens,
max_output_tokens, pricing, capabilities }` incl. cache/reasoning rates).
Current status (2026-07-02): `cost/catalog.rs` is a 622-line static pricing and
window table plus registry-enrichment/estimate helpers. The unused
`context_window` convenience wrapper is gone, the raw pricing table is private,
and `context_window_for_model` now consults the richer `lookup` row for concrete
model ids before falling back to legacy model-context patterns. OpenHuman's
`ModelPrice` currently has input, cached-input, and output rates; TinyAgents
`ModelPricing` also models cache-creation and reasoning-token rates, so the
projection must preserve those gaps explicitly rather than pretending the local
table is already a complete crate catalog. First projection helpers landed:
`tinyagents_catalog_entry_for_model` maps one static OpenHuman row to a
TinyAgents `ModelCatalogEntry`, and `tinyagents_catalog_snapshot` maps the whole
table to `ModelCatalogSnapshot`, with per-token input/cache-read/output rates and
context windows. Cache-creation/reasoning prices and most runtime capability
flags remain unset until the catalog source can own them authoritatively.
## Steps
1. Build a `ModelCatalogSnapshot` from OpenHuman's data: seed from crate
`ModelCatalog::seed()`, overlay `cost/catalog.rs` rates/windows and
remaining `model_context.rs` pattern fallbacks, plus local-model profiles discovered at
runtime (ollama). Partially done: `cost/catalog.rs` can now project one
static row into TinyAgents `ModelCatalogEntry` and all static rows into a
`ModelCatalogSnapshot`; crate-seed merging and local-model overlays remain.
2. Point consumers at the one projection: `estimate_cost_usd`
(cost bridge), token budgeting / `effective_context_window`, model picker
RPC, capability filter (02.1 profiles via
`ModelProfile::from_catalog_entry`).
3. Keep the catalog OpenHuman-refreshable (config/update path) — crate seed
is a fallback, not the source.
## Deletions
- Duplicate per-MTok rate tables and window constants outside the catalog
(`cost/catalog.rs` becomes the loader/owner of the snapshot rather than a
second schema, or is folded into it).
## Acceptance
- Pricing/window/capability lookups have exactly one code path; cost
estimates unchanged (existing catalog tests re-pointed).
- Local models appear in the catalog with runtime-discovered profiles.
@@ -1,28 +0,0 @@
# 02 — Models & Providers
Make tinyagents the model abstraction: registry-resolved models with real
profiles, SDK-owned retry/fallback, native reasoning streaming, and catalog-
driven capability/pricing data. OpenHuman keeps credentials, OAuth, billing
classification, and provider construction (`factory.rs`).
Target SDK surface: `ModelRegistry::resolve(ModelSelection)`, `ModelProfile`/
`CapabilitySet` (incl. `reasoning`), `RetryPolicy`/`FallbackPolicy` on
`RunPolicy` + `ModelFallbackMiddleware`, `ProviderError` (normalized,
`retryable` flag), `ModelStreamItem::MessageDelta { text, reasoning,
tool_call }`, `registry::ModelCatalog{,Entry,Pricing,Capabilities}`.
Steps:
1. `01-model-registry.md` — workload routes as registry entries.
2. `02-fallback-retry.md` — collapse `reliable.rs` double-retry.
3. `03-streaming-reasoning.md` — stream reasoning natively; shrink
`ThinkingForwarder` to the remaining tool-arg/non-streaming fallback seams.
4. `04-catalog.md` — one catalog for pricing/windows/capabilities.
Done when: `provider/reliable.rs` is deleted, reasoning/tool-argument streaming
rides the crate stream without OpenHuman side channels, model resolution is
capability-checked pre-dispatch, and pricing/context-window data has one source.
Keep (product): `provider/factory.rs`, `provider/router.rs` route *names*,
credential/OAuth/billing modules, `local/` daemon, `model_ids.rs` config
policy.
@@ -1,42 +0,0 @@
# 03.1 — Delete superseded context reducers
`tinyagents/middleware.rs` doc-comments already note these were "effectively
dead on the live loop" before being re-expressed as middlewares.
Current status: `harness/compaction/cache_align.rs` and the
`harness/compaction/mod.rs` shim are deleted; the volatile-token detector now
lives inside TinyAgents `CacheAlignMiddleware`.
## Steps
1. `context/microcompact.rs` (269): deleted. The shared placeholder/default
constants now live in `context/mod.rs`, and the live clearing logic is the
TinyAgents `MicrocompactMiddleware`.
2. `context/pipeline.rs` (454) + `context/guard.rs` (236): deleted. The
minimal usage/session-memory state now lives in `context/stats.rs`, and live
compression uses `ContextCompressionMiddleware` + `summarize.rs`
(`SUMMARIZE_THRESHOLD_FRACTION` preserves the 0.90 trigger).
3. `harness/compaction/cache_align.rs` (200) + `compaction/mod.rs` shim:
superseded by `CacheAlignMiddleware`; directory deleted.
4. `context/tool_result_budget.rs` (172): deleted. The UTF-8-safe fallback
truncation helper now lives beside action-workspace artifact preview
handling; the live TinyAgents path uses `ToolOutputMiddleware` per-tool
caps.
5. Legacy compaction-breaker fields in `ContextStatsState` / `ContextStats`:
deleted after exact non-test call-path checks showed no production readers.
The remaining stats projection is UI utilisation plus session-memory state.
6. Summarization provenance: ensure every compression emits
`AgentEvent::Compressed` with `SummaryRecord` data (source ids,
before/after token estimates) — wire `ContextCompressionMiddleware::
records()` into the run outcome/journal.
## Deletions
- `context/microcompact.rs`, `context/pipeline.rs`, `context/guard.rs`, and
`harness/compaction/` are deleted.
## Acceptance
- `context/` line count drops ~1.2k; `stats()` UI footer parity test green;
compression fires at 90% window on all three turn paths (existing
summarize tests).
@@ -1,35 +0,0 @@
# 03.2 — Crate cache layer
## Steps
1. **Prompt-prefix protection:** install `PromptCacheGuardMiddleware` in
`assemble_turn_harness`; declare the stable prefix with
`ModelRequest::cache_segments` (system prompt + tool schemas as
`PromptSegment`s from the prompt builder). Route `CacheLayoutEvent`s to
the event bridge as warnings — replaces the old cache_align warn-log with
structured events. Set `CachePolicy.protect_prompt_prefix = true` on the
turn `RunPolicy`.
2. **Response cache:** attach a `ResponseCache` via
`AgentHarness::with_response_cache` for deterministic internal calls
(summarizer/triage/subconscious-style runs; NOT interactive chat).
Start with `InMemoryResponseCache`; a `Store`-backed impl over
`FileStore` is a follow-up if hit rates justify it. Gate per-request
with `ModelRequest::with_cache_policy`.
3. Assert prompt-prefix stability across turns in a fixture test:
`PromptCacheLayout::from_request(...).is_prefix_stable_against(prev)`
volatile content (timestamps, memory, steering) must land in the tail.
This formalizes the prompt-cache-stability decisions currently embedded
in session prompt assembly.
4. Surface `CacheHit`/`CacheMiss` counts in the cost footer projection.
## Deletions
- `CacheAlignMiddleware`'s bespoke detector once `PromptCacheGuardMiddleware`
+ layout events cover it (keep OpenHuman's volatile-token *vocabulary* as
test fixtures).
## Acceptance
- Prefix-stability fixture green over a 3-turn session with memory injection.
- Response-cache hit serves an identical deterministic request (test with
`MockModel::call_count`).
@@ -1,32 +0,0 @@
# 03 — Context management & caching
Live context reduction already runs on `ContextCompressionMiddleware` +
`MessageTrimMiddleware` + `tinyagents/summarize.rs`; microcompact and
cache-align live as TinyAgents middlewares. This workstream deletes the superseded
originals and adopts the crate cache layer.
Current status (2026-07-02): the legacy reducers are gone. `context/` is 1237
lines total, including 1051 lines across prompt assembly (`prompt.rs`, `channels_prompt.rs`),
session-memory bookkeeping (`session_memory.rs`), and stats/config state
(`manager.rs`, `stats.rs`). The inert legacy compaction-breaker stats have also
been removed. Those files remain product-owned; cache correctness work now targets
TinyAgents middleware and crate cache events.
Target SDK surface: `ResponseCache`/`InMemoryResponseCache` + `cache_key`,
`CachePolicy { response_cache_enabled, protect_prompt_prefix }`,
`PromptCacheLayout` + `PromptCacheGuardMiddleware` + `CacheLayoutEvent`,
`AgentEvent::{CacheHit, CacheMiss, Compressed}`, `SummaryRecord`,
`ModelRequest::cache_segments(Vec<PromptSegment>)`.
Steps:
1. `01-delete-legacy-context.md` — remove dead reducers, consolidate stats.
2. `02-cache-layer.md` — response cache + prompt-prefix protection.
Done when: `context/` contains only OpenHuman-specific state (prompt
assembly inputs, session-memory bookkeeping, stats projection) and cache
correctness is asserted by crate events, not warn-logs.
Keep (product): `context/manager.rs` prompt-assembly surface,
`session_memory.rs`, `channels_prompt.rs`, `context/stats` for the UI
utilisation footer.
@@ -1,33 +0,0 @@
# 04.1 — Live turns write tinyagents records
Today new turns append to `session_raw/*.jsonl` via `transcript.rs`;
the tinyagents store only receives one-time imports.
## Steps
1. Add a session store handle (same `FileStore`/`JsonlAppendStore` layout as
`session_import`: `{workspace}/tinyagents_store/{kv,journal}`) to the
session shell; register it on `RunContext.stores` in
`assemble_turn_harness`.
2. After each turn, append the turn's messages to
`session.{stem}.messages` and upsert the session descriptor (ns
`sessions`) — dual-write alongside the legacy JSONL append in
`session/turn/session_io.rs`. Reuse `session_import/convert.rs`
normalization so live and imported records are shape-identical.
3. Adopt `StoreChatHistory` as the in-run history mechanism where the shell
currently hand-threads message vectors (evaluate: it must preserve
native tool-call envelopes and XML/P-format compat suffixes verbatim —
if not, keep OpenHuman assembly and only persist through the store).
4. Write-side parity test: run a turn, assert legacy JSONL and store stream
render identical transcripts (reuse importer's JournalMessage parity
helper).
## Deletions
None yet (dual-write phase). Deletion lands in 04.2.
## Acceptance
- Every new turn appears in the store journal with correct stem, thread_id,
provider/model metadata, tool-call ids.
- Dual-write behind one flag so 04.2 can flip reads independently.
@@ -1,43 +0,0 @@
# 04.2 — Read-side shadow, cutover, retirement
Import phases 24 from `../../tinyagents-session-migration-design.md`.
## Steps
1. **Shadow (phase 2):** add a store-backed reader implementing the same
surface as `transcript.rs` (`read_transcript`, history load in
`session_io.rs`, `.md` render). Behind a flag, read BOTH and log
divergence (grep-friendly `[session_import]` prefix); run the 11-fixture
matrix + live dogfood.
2. **Cutover (phase 3):** run `openhuman.session_import_run` automatically
once at boot (idempotent marker `migrations/session_import_v1` already
exists), flip default reads to the store, keep legacy JSONL as
write-through backup for one release.
3. **Retire (phase 4):** stop legacy writes; delete the legacy stack.
4. Sub-agent transcript stems (`__` chains) and worker-thread mirrors must
resolve through store descriptors — verify against
`subagent_runner` mirroring and `agent_orchestration/subagent_sessions/`
(that store also folds into descriptors here).
5. Parser retirement gate: keep `agent/dispatcher.rs` and
`harness/parse.rs` until no live provider path needs XML/P-format prompt
dialects, native text-fallback parsing, TinyAgents text-mode response
parsing, or sub-agent checkpoint cleanup.
## Deletions (phase 4)
- `session/transcript.rs` (1347) + `transcript_tests.rs`.
- `session/migration.rs` (373) + tests — old date-folder migration, still
invoked at startup through the `migrate_session_layout_if_needed` re-export
until the read-side cutover has a replacement startup gate.
- `session/turn/session_io.rs` (391) — replaced by store reader/writer.
- `session_db/{ops,store,schemas,types}.rs` generic session parts (keep
`run_ledger/` until 05).
- `agent_orchestration/subagent_sessions/` (~650).
- `src/openhuman/session_import/` (whole domain + RPC controller from
`core/all.rs`) once a marker-versioned release has shipped.
## Acceptance
- All fixtures byte/render-identical between readers before flip.
- Old UI session lists still answer by OpenHuman session key (compat
projection over descriptors).
@@ -1,43 +0,0 @@
# 04.3 — SqliteCheckpointer swap
Baseline is complete: OpenHuman is on tinyagents 1.5.0 with the `sqlite`
feature and the compatible `rusqlite` pin. The remaining work is the
OpenHuman-row migration/expiry decision.
Current status (2026-07-02): do not delete
`src/openhuman/tinyagents/checkpoint.rs` yet. `SqlRunLedgerCheckpointer` is still
the live durable checkpointer for delegation graphs, and it writes OpenHuman's
`graph_checkpoints` run-ledger schema. Its adapter surface is now crate-internal.
The crate `SqliteCheckpointer` uses its own checkpoint schema, so swapping it in
requires an explicit row migration or documented expiry policy for in-flight
durable graph runs.
## Steps
1. Point durable graphs at crate `SqliteCheckpointer` only after the schema
migration/expiry decision is implemented. Current production usage of
`SqlRunLedgerCheckpointer` is the delegation graph; `FileCheckpointer`
appears only in local TinyAgents delegation tests.
2. Migration for existing `graph_checkpoints` rows in the session DB:
either a one-time copy into the crate schema or documented expiry
(checkpoints are resume-state; expiring in-flight runs at upgrade is
acceptable ONLY if no long-lived durable runs exist — audit
`workflow_runs` retention first).
3. Keep `CheckpointMetadata` (thread_id/checkpoint_id/parent/namespace)
projected into the run ledger for the command-center listing RPC —
a read-side projection, not a second writer.
4. Standardize `DurabilityMode` per graph (Sync for approval-interrupt
graphs, Async for fanout).
## Deletions
- Later: `src/openhuman/tinyagents/checkpoint.rs`
(`SqlRunLedgerCheckpointer`, 250) + the `graph_checkpoints` table creation
once migration/expiry ships. Retain it while delegation still points at the
OpenHuman run-ledger schema.
## Acceptance
- Durable graph interrupt → process restart → resume from exact checkpoint
(e2e per graph).
- Command center still lists checkpoints/current node.
@@ -1,26 +0,0 @@
# 04 — Sessions & stores
Make tinyagents `Store`/`AppendStore` the durable session substrate and
retire the legacy transcript stack. The Phase-1 write-only importer already
exists (`src/openhuman/session_import/`, `openhuman.session_import_run`,
design in `../../tinyagents-session-migration-design.md`).
Target SDK surface: `Store`/`FileStore`, `AppendStore`/`JsonlAppendStore`,
`StoreRegistry` on `RunContext`, `ChatHistory`/`StoreChatHistory`,
`SessionId`/`ThreadId`/`RunId`, `HarnessRunStatus`.
Steps:
1. `01-live-writes.md` — new turns write tinyagents records first.
2. `02-read-cutover.md` — import phases 24: shadow reads, cutover, retire.
3. `03-checkpointer.md` — SqliteCheckpointer swap (needs 00-baseline).
Done when: `session/transcript.rs`, `session/migration.rs`,
`session/turn/session_io.rs`, and `session_import/` are deleted; readers
serve from `tinyagents_store/`; run inspection uses crate lineage ids.
CONSTRAINT (from importer work): store/stream names are slash-free —
per-session streams are `session.{stem}.messages`.
Risk: transcript shape is user data — every cutover step keeps the legacy
reader until parity fixtures (11-fixture matrix in the design doc) pass.
@@ -1,30 +0,0 @@
# 05.1 — Durable journals + status stores
## Steps
1. In `assemble_turn_harness`, attach a `StoreEventJournal` (over the
04-sessions `JsonlAppendStore`) via a `JournalSink` on the run's
`EventSink`, plus a `HarnessStatusStore` writer (start with the
in-memory impl process-wide; durable Store-backed status is the target —
write a small `Store`-backed `HarnessStatusStore` impl if the crate has
none).
2. Same for graphs: `JournalGraphSink` + `GraphStatusStore` on delegation/
workflow/teams/fanout runs.
3. Wrap UI-bound sinks with `RedactingSink`; define the redaction policy
(prompts, tool args/results, secrets) in one OpenHuman module.
4. Late-attach replay: an RPC (`agent.run_events`?) that reads
`read_from(run_id, offset)` + status so the desktop can reconnect
mid-run and backfill — replaces reliance on transient `AgentProgress`
buffering.
5. Feed `session_db/run_ledger` event/telemetry rows FROM journal records
(projection job or write-through sink) instead of parallel publishes.
## Deletions
None directly; enables 05.2/05.3 deletions.
## Acceptance
- Kill the UI mid-turn, reattach, reconstruct the full timeline from
journal + status (e2e).
- `list_by_root` answers "every active descendant of this root run".
@@ -1,39 +0,0 @@
# 05.2 — One event bridge; delete engine/
Two progress paths remain: `OpenhumanEventBridge`
(`tinyagents/observability.rs`) and scattered direct `publish_global` calls in
session/turn code. The old shared `harness/engine/` progress/checkpoint seams
are gone.
## Steps
1. Done for the reusable engine module: `ProgressReporter`/`TurnProgress` moved
under `session/tool_progress.rs`, leaving no shared `harness/engine/`
surface. The old session-local `agent_tool_exec` compatibility shim is
deleted.
2. Done: `engine/checkpoint.rs` is gone. `CapPauser` owns the graceful
max-iteration stop, and the remaining sub-agent checkpoint summary is
localized in `subagent_runner/ops/checkpoint.rs`.
3. Sweep direct agent-run/tool/subagent lifecycle `DomainEvent` publishes on
turn paths (session/turn, orchestration tools) — emit
through the bridge or a typed helper so ordering/rate-limiting is
single-owner.
4. Current finding (2026-07-02): `agent/progress_tracing.rs` (722) plus
`agent/progress_tracing/tests.rs` (616) are not currently redundant. The
root module is the opt-in `observability.agent_tracing` exporter fed by the
web progress bridge's `AgentProgress` stream. Retain it until the 05.1
journal path can produce the same metadata-only OTel/Langfuse spans and
append/export contract.
## Deletions
- Deleted: `src/openhuman/agent/harness/engine/` (checkpoint + progress seams).
- Redundant publishes found in step 3.
- Retain `agent/progress_tracing.rs` + sibling tests for now; delete only after
journal-backed span export proves parity with the current config contract.
## Acceptance
- Progress-event parity fixture: same `AgentProgress` sequence for a
scripted turn before/after (tool timeline, thinking, cost footer).
- No `engine::` imports remain.
@@ -1,38 +0,0 @@
# 05.3 — DomainEvent & AgentProgress as projections
`DomainEvent` (core event bus) and `AgentProgress` (UI vocabulary) stay —
as compatibility projections fed from crate events, never as primary state.
## Steps
1. Catalog current agent-domain `DomainEvent` publishers/subscribers
(`agent/bus.rs`, triage, task_dispatcher, orchestration
background_delivery/run_ledger_finalize). Current high-volume lifecycle
publishers still include `Agent::run_single`, `SpawnSubagentTool::execute`,
`ContinueSubagentTool::execute`, `dispatch_subagent`,
`project_spawn_parallel_spawned`, and `project_spawn_parallel_result`.
`RunLedgerFinalizeSubscriber::handle` remains a real global-bus subscriber:
it consumes `SubagentCompleted`/`SubagentFailed` as authoritative detached-run
terminal signals until journal/status-store replay covers those paths. For
each event that mirrors a crate `AgentEvent`/`GraphEvent`, source it from the
journal/bridge. Events with no crate analogue (triage decisions, channel
routing) stay native DomainEvents — they are product semantics.
2. `agent/bus.rs` native handler `agent.run_turn` stays (transport into the
turn), but its progress mirroring moves to the bridge. Current live bridge
symbols are `OpenhumanEventBridge::on_event` plus `ThinkingForwarder` for the
non-streaming/tool-argument gaps that do not yet have crate event parity.
3. Document the rule in `core/event_bus` README: new agent-run telemetry
reads crate events/status first; DomainEvent is for cross-domain product
signals.
4. Do NOT remove `DomainEvent` variants until every subscriber is re-pointed
(spec non-goal) — track per-variant status in this file as work lands.
## Deletions
- Duplicate mirroring blocks identified in step 1 (list them here when
cataloged).
## Acceptance
- Existing subscribers receive identical DomainEvents (bus tests).
- New run-inspection RPCs read journals/status, not the bus.
@@ -1,24 +0,0 @@
# 05 — Events, status, observability
Make crate journals/status stores the canonical run record; everything
OpenHuman-specific becomes a projection (AgentProgress, DomainEvent, run
ledger, cost footer).
Target SDK surface: `HarnessEventJournal` (`StoreEventJournal` over
`JsonlAppendStore`), `HarnessStatusStore` (`list_by_root`, `list_by_thread`,
`list_active`), `AgentObservation { run_id, parent_run_id, root_run_id,
offset }`, `GraphEventJournal`/`GraphStatusStore`, `RedactingSink`,
`FanOutSink`, `RecordingListener`, Langfuse exporters (optional).
Steps:
1. `01-journals.md` — persist journals + status; late-attach replay.
2. `02-bridge-consolidation.md` — one bridge; delete `engine/`.
3. `03-domainevent-projection.md` — DomainEvent/AgentProgress as projections.
Done when: a UI can reconstruct a running/completed turn from persisted
crate events without subscribing at start; run-ledger event rows are fed from
journal records. `agent/harness/engine/` is already deleted.
Risk: crosses UI streaming, cost footer, desktop reconnect — parity tests
before each deletion.
@@ -1,87 +0,0 @@
# 06 — Usage, cost, budgets
Finish moving accounting onto crate primitives; OpenHuman keeps the tracker
DB, dashboard RPC, and pricing policy.
Target SDK surface: `Usage { input_tokens, output_tokens, total_tokens,
cache_read_tokens, cache_creation_tokens, reasoning_tokens }`, `UsageTotals`,
`CostTotals`, `BudgetMiddleware`/`BudgetLimits`/`BudgetTracker`,
`AgentEvent::{UsageRecorded, CostRecorded, BudgetReserved, BudgetReconciled,
BudgetWarning, BudgetExceeded}`,
`ChildRun.usage`/`RunTree` lineage rollup, `ModelPricing` (catalog, incl.
cache/reasoning rates).
The $0-cost bug and the unobserved-turn tracker gap are already fixed.
Remaining:
Current status (2026-07-02): tinyagents 1.3.0 exposes the target budget and
usage primitives, but OpenHuman has not wired `BudgetMiddleware`,
`BudgetLimits`, `ChildRun`, or `RunTree` into the shared runner. TinyAgents
runtime already emits `AgentEvent::UsageRecorded` after each model call, so the
OpenHuman gap is ownership/de-duplication of that event stream, not a separate
usage-accounting middleware install. Keep the current OpenHuman cost seams live
for now:
crate-internal `CostBudgetMiddleware` gates already-exceeded daily/monthly
budgets on every shared turn, `OpenhumanEventBridge::record_usage` records
`UsageRecorded` into the global tracker, and crate-internal
`turn_subagent_usage` folds child spend into the parent turn footer and
in-memory `LastTurnUsage` web footer payload. Transcript/session metadata
persistence is separate. The bridge now logs TinyAgents budget/cost events
(`BudgetReserved`, `BudgetReconciled`, `BudgetWarning`, `BudgetExceeded`,
`CostRecorded`) without feeding them into OpenHuman accounting. Installing the
crate budget middleware before event de-duplication would risk double-counting
`UsageRecorded`.
Local inventory: there is no local `src/openhuman/tinyagents/cost*` adapter
module; TinyAgents itself has `tinyagents::harness::cost`. The current local
seams are crate-internal `tinyagents/middleware.rs::CostBudgetMiddleware`
(inside the 1861-line middleware module), crate-internal
`agent/harness/turn_subagent_usage.rs` (176) for task-local parent-turn rollup,
and crate-internal `agent/cost.rs::TurnCost` for the web footer payload, budget
stop hooks, and legacy progress compatibility.
## Steps
1. **Normalize records:** carry `cached_input_tokens`, cache-creation tokens,
`reasoning_tokens` (crate `Usage` has it), image/audio/embedding usage where
providers report; add `cost_source: estimated | provider_charged` to
`TokenUsage`; keep provider-charged USD via an out-of-band carry (crate
`Usage` has no USD field — residual upstream gap). Current `TokenUsage`
persists cached-input provenance and `cost_source`; reasoning and
cache-creation tokens remain zero until providers report them.
2. **Crate budget middleware:** replace bespoke `CostBudgetMiddleware`
daily/monthly pre-checks with `BudgetMiddleware` where limits map; add
per-run and per-thread budgets (new `CostConfig` fields + thread-id
threading). TinyAgents `BudgetTracker` is run/tree-local, so it does not
directly replace OpenHuman's persistent daily/monthly `CostTracker`
semantics. Preflight `BudgetExceeded { blocked: true }` fails a model call
before dispatch; post-spend `BudgetExceeded { blocked: false }` is only an
event unless OpenHuman installs an explicit pause/failure policy. 1.3.0 also
emits `BudgetReserved`/`BudgetReconciled`, but current preflight estimates
input tokens and blocks against configured limits rather than pre-reserving
cached tokens or projected USD for the next provider call. First build an
OpenHuman limits adapter and define `UsageRecorded` ownership so the bridge,
crate accounting middleware, and unobserved-turn fallback cannot record the
same model call twice.
3. **Lineage rollup:** stamp cost records with `run_id`/`root_run_id` from
the observation stream (needs `TokenUsage` schema fields); parent totals
via `UsageTotals`/`ChildRun` instead of the `turn_subagent_usage`
task-local where lineage suffices; keep the task-local only for detached
children the tree can't see (or move them to TaskStore rollup — 07.2).
4. **Embedding usage:** embedding calls record usage/cost with provider,
model, dimensions, vector count (ties into 09).
## Deletions
- Later only: crate-internal `CostBudgetMiddleware` after crate budget mapping
covers OpenHuman daily/monthly stop behavior, `agent/cost.rs::TurnCost` only
where `UsageTotals` covers all current web-footer, stop-hook, TokenJuice, and
audit callers, and `turn_subagent_usage.rs` only after 07.2 rollup parity. Do
not delete any of these until duplicate `UsageRecorded` semantics are resolved
and run-tree/thread-budget integration covers the current stop-hook and
parent-turn rollup paths.
## Acceptance
- Dashboard totals identical before/after on a scripted multi-child turn
(no double count); budget-exceeded stops a recursive run pre-spend.
@@ -1,56 +0,0 @@
# 07.1 — Sub-agent pipeline as a subgraph
Convert `subagent_runner/` "prepare prompt → filter tools → run child →
checkpoint/handback → mirror transcript" into an explicit graph.
Current status (2026-07-02): `tinyagents/subagent_graph.rs` defines and exports
the fixed pipeline topology, and `run_typed_mode` now runs that graph as a
best-effort diagnostic skeleton before continuing through the procedural runner.
The skeleton records the named phases with `GraphTracingSink`; the node effects
remain pending and can be moved over one phase at a time without changing the
external sub-agent behavior. The live child turn loop is still
`run_subagent_via_graph`: it already uses the shared TinyAgents harness for
steering, early exit, cap summaries, transcript persistence, and usage
aggregation, but it is not yet a `SubAgentSession`/`subagent_node` graph
implementation. The standalone `ops/usage.rs` glue file is deleted; the
remaining `AggregatedUsage` bridge lives with the graph route that produces it,
and the oversized-result `apply_handoff` helper now lives beside
`ResultHandoffCache` in `handoff.rs` instead of under `ops/`.
## Steps
1. Partially done: define `build_subagent_pipeline_graph` (now in
`tinyagents/subagent_graph.rs` or under `subagent_runner/`): nodes
`resolve_definition` (registry lookup + allowlist) → `prepare_context`
(parent ctx, memory, action root, sandbox) → `assemble_prompt`
`expose_tools` (uses 01.3 selection middleware) → `run_child`
(harness leaf via `subagent_node` or direct
`run_turn_via_tinyagents_shared`) → `finalize` (checkpoint/
awaiting-user handback, worker-thread mirror, handoff cache).
Follow the established `build_*_graph` + `*_topology()` pattern; topology
export exists, but node effects are still diagnostic placeholders.
2. Partially done: `run_typed_mode` (`ops/runner.rs`, the single chokepoint)
invokes the compiled subgraph for diagnostic tracing before continuing
through the procedural runner; `AgentGraph::Custom` runners still plug in as
alternate `run_child` leaves.
3. Child lineage: run with parent depth/events (`invoke_in_parent`
semantics) so `root_run_id` rollup + `SubAgentStarted/Completed/Reused`
events are native; usage rollup via `ChildRun.usage` (feeds 06.3).
4. Fold `ops/{provider,prompt,checkpoint}.rs`
plumbing into node implementations; `ops/graph.rs` shrinks to the leaf.
5. Reusable child sessions: map the follow-up/continue flows onto
`SubAgentSession` (`send`, `transcript()`, `reset()`).
## Deletions
- Deleted: `subagent_runner/ops/usage.rs` glue.
- Deleted: `subagent_runner/ops/handoff_helper.rs` glue; `apply_handoff` moved
to `subagent_runner/handoff.rs`.
- Remaining: parts of `ops/runner.rs`/`ops/graph.rs` absorbed by nodes (target:
`ops/*` shrinks to policy nodes + tests).
## Acceptance
- Sub-agent e2e parity (ops_tests 1689-line suite green or rewritten
against graph events); topology export validates; checkpoint/handback
and worker-thread mirroring unchanged from the outside.
@@ -1,72 +0,0 @@
# 07.2 — Detached sub-agents on durable TaskStore
`running_subagents.rs` already mirrors lifecycle into the crate TaskStore but
still owns watch channels, abort handles, task lookup, and ownership checks. The
crate now has `JsonlTaskStore` (durable), `OrchestrationTaskSpec::with_lineage`,
filters, and a `SteeringRegistry`.
Current status (2026-07-02): detached sub-agent lifecycle records now open a
per-workspace durable `JsonlTaskStore` at
`<workspace_dir>/.openhuman/orchestration_tasks.jsonl` on first spawn, falling
back to `InMemoryTaskStore` only if that workspace log cannot be created/opened.
Records carry TinyAgents lineage (`parent_run_id`/`root_run_id`), timeout,
parent session, parent thread, durable `subagent_session_id`, and workspace
metadata, and terminal/cancelled mirrors now resolve the same workspace-scoped
store that recorded the spawn. `wait_subagent` still prefers the live registry,
but can now resolve `subagent_session_id`, resume metadata, and terminal or
still-running status from the workspace-scoped TaskStore after a live-registry
miss. `steer_subagent` now uses the same workspace-scoped session-id resolver
before delivering to the live executor. Reusable-session close/fresh paths also
resolve session cancellation through the workspace store before aborting any live
executor. The rest of the executor/control path still uses OpenHuman's watch
channels, abort handles, task lookup, and `RunQueue`; the unused future
`running_subagents::close` hook has been removed and the test-only typed ledger
snapshot plus finished background completion/delivery queues are crate-internal.
`running_subagents` and `subagent_sessions` are now crate-only; the live
`harness-subagent-audit` binary uses the narrow public `harness_audit` facade
for durable session reads and mid-run steer probes. Restart reconciliation,
durable store polling, and steering-registry replacement remain pending.
## Steps
1. Done: detached lifecycle now opens `JsonlTaskStore::open` under the
workspace store dir, with `InMemoryTaskStore` only as the open-failure
fallback. Task records now carry lineage
(`with_lineage(parent_run_id, root_run_id)`), the default detached wait
timeout, and thread/session metadata.
2. In progress: `wait_subagent` now falls back to TaskStore reads after the
live registry misses, `list_subagents` overlays stale durable session
summaries with TaskStore status for each `current_task_id`, and
`steer_subagent` resolves durable session ids from the same workspace store
before attempting live delivery. Close/fresh reusable-session paths use the
same workspace-scoped session lookup before cancelling a live task. Continue
re-expressing controls on crate semantics: wait →
`orchestrate_await`-style store polling/wait handle; cancel/kill →
`CancellationToken` + terminal record; steer →
`SteeringRegistry` (`TaskId → SteeringHandle`) replacing the RunQueue lookup
plumbing. Keep abort-handle hard-kill as the OpenHuman executor detail.
3. Keep OpenHuman ownership checks + durable session rows as policy over
the store; cancelled/failed states become terminal `OrchestrationTaskRecord`s.
4. Restart/resume: on boot, reconcile `JsonlTaskStore` live records against
actual executors (orphans → failed-with-restart marker); prove desktop
restart parity vs today's behavior. Run the crate's 1.3.0 testkit
contracts (`taskstore_concurrent_contract`, `taskstore_replay_contract`)
against the chosen store.
5. Evaluate exposing crate `orchestrate_*` tools to the orchestrator agent
as the internal engine under `spawn_agent`/`wait_agents`/etc. — the
product tools stay as the model-visible surface (names/output shapes are
product contract).
6. While here: root-cause the 2 known-failing detached-orchestrator e2e
hangs (`e2e_orchestrator_answers_coding_agent_question...` — child stays
Running past timeout; see memory 2026-06-30f).
## Deletions
- Watch-channel/task-lookup mechanics in `running_subagents.rs` (1250)
(target ≤ ~300 lines of policy + executor glue).
## Acceptance
- Spawn detached → restart process → list/wait/cancel still work.
- running_subagents suite (12) + orchestration tools suite (111) green,
incl. the 2 currently-failing e2e.
@@ -1,70 +0,0 @@
# 07.3 — Steering surface + one recursion authority
## Steering
Current status (2026-07-02): `harness/run_queue/` is still live. OpenHuman
drains its own `Steer`/`Collect` lanes and forwards them as TinyAgents
`SteeringCommand::InjectMessage`; TinyAgents drains those `SteeringHandle`
commands at loop checkpoints. The queue is also the product adapter for
web-channel `followup` and `parallel` semantics and for detached sub-agent
steer/collect lookup through `running_subagents`. The local
`openhuman::tinyagents::orchestration` seam now re-exports `SteeringRegistry`,
`TaskId`, `SteeringCommand`, `SteeringPolicy`, and `SteeringHandle`, and
sub-agent TinyAgents runs register their live `SteeringHandle` in a
process-local `SteeringRegistry` while the run is active. Detached
`steer`/`collect` controls now prefer that registry handle and fall back to
`RunQueue` when no handle is available. The shared OpenHuman turn seam installs
a local steering policy that accepts only `InjectMessage` and `Pause` until a
product control surface owns additional crate command kinds. Do not delete the
directory until the remaining detached control modes and the web-channel
followup/parallel lanes either have TinyAgents-owned equivalents or move into
local owners. Hard cancel/prune paths also deregister task handles so aborted
runs do not leave stale registry entries.
The unused `DomainEvent::RunQueueMessageDelivered` projection has been removed;
queued/interrupt/followup events remain live.
1. Map product tools onto crate commands: `steer_subagent`
`SteeringCommand::InjectMessage` via the registered `SteeringRegistry` is
live for active TinyAgents sub-agent runs, with `RunQueue` fallback. Next:
map redirect/pause/resume/cancel modes to corresponding variants while
preserving delivery-at-safe-boundaries semantics (crate drains before each
model call).
2. Tighten policy by run class: the shared turn seam now installs an
`InjectMessage`/`Pause` allowlist; future background-only runs can accept
Cancel without also accepting transcript injection.
3. Accepted/rejected steering emits `AgentEvent::Steered`; the bridge now logs
accepted/rejected command kinds, and UI projection remains pending. Delete
bespoke acknowledgment plumbing in `run_queue/` after parity.
4. `harness/run_queue/` (317 total; 174 non-test): first split it by ownership.
Detached sub-agent `Steer`/`Collect` should collapse to a `SteeringRegistry`
lookup/registration path; web-channel `Followup`/`Parallel` remain product
turn orchestration unless a crate-owned replacement is introduced.
## Recursion
5. One cap: crate `RunLimits.max_depth`/`RecursionPolicy` becomes
authoritative; crate-internal `spawn_depth_context.rs` (66) becomes a
reader/projector of crate depth (`ToolExecutionContext.depth`) for product
error wording, or is deleted if the wording can wrap
`TinyAgentsError::SubAgentDepth`. Current code now threads
`MAX_SPAWN_DEPTH = 3` into TinyAgents `RunPolicy.limits.max_depth`,
`RunConfig.max_depth`, and MCP `agent.run_subagent` via
`mcp_server/subagent_depth.rs`, but OpenHuman's own
`spawn_depth_context` still rejects beyond the same cap before the
TinyAgents run.
6. One error shape: `SubAgentDepth`/`RecursionLimit` now preserve the existing
`SpawnDepthExceeded` error surface before the subagent graph wraps provider
failures.
## Deletions
- `harness/run_queue/` queue mechanics, only after detached steer/collect no
longer use `Arc<RunQueue>` and web-channel followup/parallel behavior has a
replacement or local owner.
- crate-internal `harness/spawn_depth_context.rs` (if step 5 wording-wrap
suffices).
## Acceptance
- Mid-flight steer lands only at loop boundaries (existing steering tests);
depth-exceeded produces the same user-facing error as today.
@@ -1,28 +0,0 @@
# 07 — Sub-agents, steering, recursion
Re-express the sub-agent pipeline on crate primitives; collapse the
detached-run registry onto durable task stores; one recursion authority.
Target SDK surface: `SubAgent::invoke_in_parent` (lineage/depth/event
inheritance), `SubAgentSession` (multi-turn reuse), `SubAgentTool` (depth
guard), `graph::subagent_node` + `SubAgentPolicy`/`SubAgentBudget`,
`graph::subgraph` (`shared_subgraph_node`/`adapter_subgraph_node`),
`TaskStore`/`JsonlTaskStore` + 10 `orchestrate_*` tools +
`SteeringRegistry`, `SteeringCommand`/`SteeringHandle`/`SteeringPolicy`,
`RecursionPolicy`/`RecursionStack`, `CancellationToken`.
Steps:
1. `01-subagent-pipeline.md` — build pipeline as a subgraph.
2. `02-detached-taskstore.md` — running_subagents onto durable TaskStore.
3. `03-steering-recursion.md` — steering tools + one recursion cap.
Keep (product): agent definition lookup, allowlist enforcement, archetype
prompt assembly, memory context, sandbox/action-root narrowing,
worker-thread mirroring, integrations preflight, handoff cache. These become
named nodes/adapters, not deleted.
NOTE (memory, P5 2026-06-30): wrapping crate `SubAgentTool` around a bare
`Arc<AgentHarness>` was DECLINED because it discarded the pipeline. The
subgraph approach keeps the pipeline as explicit nodes — that's the
difference that makes it viable now.
@@ -1,48 +0,0 @@
# 08.1 — `run_parallel_fanout` → `graph::parallel::map_reduce`
The crate now has the ordered/bounded map-reduce helper the OpenHuman
helper was written to fill (`ParallelOptions`, `FailurePolicy`,
per-item `ItemOutcome`, cancellation).
Current status (2026-07-02): landed. Live fanout callers now invoke
`tinyagents::graph::parallel::map_reduce` directly: `spawn_parallel_agents`,
workflow phase fanout (`workflow_runs/engine.rs`), and model_council fanout.
`tinyagents/orchestration.rs::run_parallel_fanout` has been removed. The
remaining TaskStore re-export seam is crate-internal for detached-subagent
lifecycle bookkeeping. Workflow phase fanout now shares the durable workflow
stop signal with `ParallelOptions::with_cancellation`, so SDK cancellation maps
back to the existing `Interrupted` run state. Usage-rollup parity has direct
coverage in `turn_subagent_usage::map_reduce_fanout_preserves_scope`: TinyAgents
1.3.0 `map_reduce` drives bounded futures through `buffer_unordered` and
reorders in the same async call path, so OpenHuman's tokio task-local
parent-turn collector stays visible to inline `spawn_parallel_agents` workers.
## Steps
1. Done: compare `tinyagents/orchestration.rs::run_parallel_fanout` semantics
(input-order results, bounded concurrency, take-once payload cell,
graph events) with `map_reduce`.
2. Done: re-point callers: `spawn_parallel_agents`, workflow phase intra-phase
fanout (`workflow_runs/engine.rs`), model_council fanout.
3. Done: preserve the task-local usage-collector behavior. The direct
`turn_subagent_usage::map_reduce_fanout_preserves_scope` regression proves
records made inside `map_reduce` workers still roll into the active parent
turn collector. The longer-term 06-cost work can replace this task-local
bridge with `UsageTotals`/`ChildRun` lineage once lineage parity lands.
4. Done: choose `FailurePolicy` per caller (council = collect-all; workflow
phase = fail-fast or per-phase config). Use the 1.3.0 options:
`with_item_timeout`/`with_total_timeout`/`with_cancellation`
(a per-item timeout is behavior the old helper never had — set it
deliberately per caller, not by default). Workflow phase fanout now uses
`with_cancellation`; timeout policy remains unset by design.
## Deletions
- `run_parallel_fanout` + the move-once cell machinery in
`tinyagents/orchestration.rs` (file keeps the TaskStore re-exports until
07.2 relocates them).
## Acceptance
- Deterministic input-order results with out-of-order completion; task-local
usage rollup parity on a fanout turn.
@@ -1,75 +0,0 @@
# 08.2 — `spawn_parallel_agents` as a graph tool
The spec's Phase 6 node shape is now live on SDK graph helpers; remaining work
is parity evidence and cancellation coverage.
Current status (2026-07-02): `spawn_parallel_agents` already fans prepared
workers through `tinyagents::graph::parallel::map_reduce`, exports the fixed
`validate -> dispatch -> worker -> collect -> finalize` topology, and the graph
entrypoint now owns `Config.action_dir` resolution for worktree-isolated workers.
Each result now carries explicit per-task lineage (`parentSession`,
`rootSession`, `childTaskId`) so graph/status consumers can connect child task
ids back to the root run. The graph now owns the live
`validate`/`dispatch`/`worker`/`collect`/`finalize`
phases: the graph entrypoint resolves parent context and registry state,
validate parses task requests and enforces the parent `max_parallel_tools`
limit, dispatch validates/preflights workers from an owned agent-definition
snapshot, and dispatch now rejects shared-workspace workers that can use
write/execute tools unless the target definition is `sandbox_mode = "read_only"`,
the task explicitly requests `isolation = "worktree"`, or the task provides
non-overlapping `files:` ownership for the shared-workspace serial fallback.
Worker fanout still uses the SDK `map_reduce` helper for parallel-safe batches,
shared write fallback runs the prepared batch in deterministic task order and
now checks the graph cancellation token between serial workers, collect still
projects compatibility `DomainEvent`/`AgentProgress`, and finalize still
returns the existing JSON shape. Parallel batches pass the same token into SDK
`ParallelOptions::with_cancellation`, so cancelled map-reduce runs surface as a
graph `Cancelled` outcome instead of an opaque fanout error. The tool wrapper
now inherits the live TinyAgents run cancellation token through OpenHuman's
scoped harness context and passes it into the graph entrypoint, falling back to a
local token only for direct/manual tool calls outside a run. The tool wrapper
still owns `ToolResult` translation so
malformed-argument and public error shapes stay unchanged. The unused pre-graph
public wrappers have been removed, internal graph helpers have been narrowed,
and the remaining shrink target is the 1280-line graph implementation.
## Nodes
- `validate`: parse tasks, enforce min/max count, and preserve malformed-args
error ordering before parent-context lookup.
- `dispatch`: resolve per-task agent definitions, `subagents.allowlist`,
toolkit requirements, ownership prompt boundaries, and worktree preflight.
- `worker`: the 07.1 subagent pipeline subgraph with inherited policy,
optional `worktree_action_dir`, child task id, bounded budgets
(`SubAgentBudget`).
- `collect` (reducer/`ChannelSet`): successes, failures, elapsed,
iterations, worktree status, changed files, stale-read markers —
deterministic task order.
- `finalize`: compatibility `DomainEvent`/`AgentProgress` projections,
overlap warnings, existing `parallel_agents` JSON shape.
## Steps
1. Done: `build_spawn_parallel_graph` + topology export (established pattern).
2. Done: tool wrapper in `agent_orchestration/tools/spawn_parallel_agents.rs` is now
a thin shell: schema → run graph → translate `ToolResult`.
3. Partially done: policy (spec "ownership and scheduling"): read-only workers
may share workspace; worktree-isolated writers stay on `map_reduce`;
shared-workspace writers with disjoint `files:` ownership are admitted but
force a deterministic serial batch fallback; shared writers without
parseable/disjoint ownership are rejected. Cancellation now has a graph-level
token seam, named-node boundary checks, serial fallback checks between
workers, SDK map-reduce cancellation wiring, and live parent run cancellation
inheritance through the tool path. No widening of tools/model/sandbox/budget.
4. Checkpointing optional (fanout = Async durability).
## Deletions
- Orchestration mechanics inside `spawn_parallel_agents.rs` have moved into
`agent_orchestration/spawn_parallel_graph.rs`; overlap detection lives in the
collect/finalize graph path.
## Acceptance
- Required before completion: spawn_parallel suite green against identical JSON output;
status consumers render per-task lineage.
@@ -1,27 +0,0 @@
# 08.3 — Approvals as durable graph interrupts
Today approval pauses are steering-channel pauses (park the turn) and the
approval gate parks interactive chat turns with a 10-min TTL.
## Steps
1. For durable graphs (delegation review gate, workflow human-review
phases): emit `NodeResult::Interrupt { id, node, payload }` with the
approval request as payload; persist via checkpointer (Sync durability).
2. Resume path: approval RPC decision → `Command { resume: Some(decision) }`
at the stored `ResumeTarget`; process restart between request and
decision must survive (that's the point).
3. Keep the interactive chat-turn approval gate as-is (steering pause) —
chat turns are not durable graphs; document the boundary here.
4. Surface pending interrupts in command center (`GraphRunStatus` +
interrupt records); TTL expiry → resume-with-deny.
## Deletions
- Bespoke pause/park bookkeeping in workflow/delegation paths where the
interrupt record replaces it.
## Acceptance
- e2e: delegation review pause → restart process → approve → run resumes
from exact checkpoint; deny/TTL paths covered.
@@ -1,44 +0,0 @@
# 08.4 — Per-agent graphs: stubs out, bespoke in
Default-only `agent_registry/agents/*/graph.rs` stubs return
`AgentGraph::Default` (~13 lines each). Delete the boilerplate; keep the
seam; land at least one real custom graph before declaring the migration
complete so the cleanup does not remove an unproven seam.
Current status (2026-07-02): `BuiltinAgent.graph_fn` is optional and the
loader supplies `AgentGraph::Default` when it is absent. All 32
`agent_registry/agents/*/graph.rs` default stubs are gone, along with the five
default-only non-registry graph modules (`tinyplace_agent`, skill setup,
skill executor, agent memory, subconscious). `researcher` is now the first
bespoke production per-agent graph: it resolves to `AgentGraph::Custom`, runs a
compiled `route_research -> run_research_turn -> finalize` topology, exports
that topology through `agent.graph_topologies`, and delegates the actual
model/tool loop to the shared default sub-agent leaf so transcript persistence,
progress, handoff, cap-summary, and usage-rollup parity stay intact. Other
production orchestration graph topologies (delegation/workflow/team/
spawn-parallel) also exist.
## Steps
1. Done: land the first bespoke per-agent graph. `researcher/graph.rs` is an
`AgentGraph::Custom` runner over a compiled graph with topology export.
Remaining route sophistication: split search/read/synthesize/cite decisions
into richer nodes once those policies move out of prompt text.
2. Done: make `graph_fn` optional on `BuiltinAgent` (default =
`AgentGraph::Default` supplied by the loader/registry); delete every default
stub `graph.rs` whose agent has no custom graph.
3. Done: `agent.graph_topologies` returns an `agents` array showing which graph
(`default`/`custom`) each built-in resolves to.
## Deletions
- Deleted: all default-only `agent_registry/agents/*/graph.rs` files and the
`graph_fn` boilerplate wiring for them.
- Deleted: default-only graph modules for `tinyplace_agent`, `skill_setup`,
`skill_executor`, `agent_memory`, and `subconscious`.
## Acceptance
- Registry loads all agents with correct graph resolution (test);
first bespoke graph is in production and exported; richer route tests +
topology snapshot remain before declaring 08.4 complete.
@@ -1,97 +0,0 @@
# 08.5 — Worktrees behind `WorkspaceIsolation`
The crate has the isolation seam the sdk-gaps doc asked for:
`WorkspaceIsolation` trait, `WorkspaceDescriptor { root, trusted_roots,
policy_id, sandbox }` (+ `allows(path)`), `ToolExecutionContext.workspace`,
and `WorkspacePrepared/Violation/Cleanup` events.
Current status (2026-07-02): do not delete
`src/openhuman/agent/harness/worktree_context.rs` yet. `spawn_parallel_agents`
still threads `worktree_action_dir` into the sub-agent runner as a fallback,
async subagent session roots now prefer `ToolExecutionContext.workspace`, and
shell/git acting tools read the task-local `current_action_dir_override()` to
execute inside the isolated checkout when no descriptor is present. The module
is now hidden behind crate-internal harness re-exports. Its task-local carrier
and accessor helpers are no longer public beyond the crate.
`agent_orchestration::worktree::GitWorktreeIsolation` now implements the
TinyAgents `WorkspaceIsolation` trait over OpenHuman's existing git-worktree
create/remove policy and returns `WorkspaceDescriptor` roots for isolated
workers. Sub-agent run options now carry an optional `WorkspaceDescriptor`, and
the TinyAgents shared turn seam attaches it to `RunContext::with_workspace`.
For current `spawn_parallel_agents` worktree workers, dispatch now calls
`GitWorktreeIsolation::prepare` and carries the returned `WorkspaceDescriptor`
into `SubagentRunOptions`; the old `worktree_action_dir` task-local remains as
the live fallback until every acting tool reads the descriptor directly.
`spawn_parallel_agents` also reads the parent `ToolExecutionContext.workspace`
at the tool boundary, so shared workers inherit the caller workspace root and
isolated fanout workers prepare relative to that root before falling back to
`Config.action_dir`. `spawn_subagent` now preserves the same descriptor when it
routes to reusable async delegation and when a blocking sub-agent run is
requested, and `continue_subagent` passes the current caller descriptor into
resumed checkpoint runs. `spawn_worker_thread` does the same for persisted
worker-thread sub-agent runs. The archetype and integrations delegation tools
now also forward the descriptor through their shared sub-agent dispatcher, and
`agent_prepare_context` passes it into the read-only context scout when invoked
as a tool. `call_memory_agent` and `delegate_to_personality` likewise carry the
descriptor into their spawned sub-agents.
The TinyAgents tool adapter now forwards `ToolExecutionContext` into OpenHuman
tools, and shell/git plus core filesystem tools (`file_read`, `list`,
`file_write`, `edit`, `apply_patch`, `grep`, `glob`, `csv_export`) and
shell-family runtime tools (`node_exec`, `npm_exec`) use
`ToolExecutionContext.workspace.root` as their effective action directory when
present. Generated media tools (`media_generate_image`, `media_generate_video`)
also persist artifacts under that descriptor root for isolated fanout workers.
Codegraph tools (`codegraph_index`, `codegraph_search`) likewise resolve their
repo boundary and index store from the descriptor root during TinyAgents runs.
For shell and the runtime tools this also covers sandbox policy and
sandbox cwd. Delete the legacy task-local only after the remaining acting tools
also resolve roots from the carried crate `WorkspaceDescriptor`.
## Steps
1. Partially done: implement `WorkspaceIsolation` over
`agent_orchestration/worktree.rs` (git-worktree create/cleanup, dirty-
worktree safeguards stay OpenHuman policy). `spawn_parallel_agents` now uses
the adapter's prepare path and passes the descriptor into the runner;
cleanup remains a separate dirty-worktree policy slice.
2. In progress: thread `WorkspaceDescriptor` into tool execution. TinyAgents
owns the descriptor carrier, while acting tools resolve their allowed root
from `ToolExecutionContext.workspace` instead of task-local
`worktree_context.rs`/action-dir globals. The carrier is now threaded
through sub-agent run options, `RunContext::with_workspace`, the OpenHuman
tool adapter, `spawn_subagent`, `continue_subagent`, `spawn_worker_thread`,
archetype/integrations delegation dispatch, `spawn_async_subagent`
reuse/session roots, `spawn_parallel_agents` shared-worker roots,
`agent_prepare_context`, `call_memory_agent`, `delegate_to_personality`,
`delegate_graph`, shell, git operations, `file_read`, `list`, `file_write`,
`edit`, `apply_patch`, `grep`, `glob`, `csv_export`, `read_diff`,
`run_linter`, `run_tests`, `update_memory_md`, `node_exec`, `npm_exec`,
`media_generate_image`, `media_generate_video`, `codegraph_index`, and
`codegraph_search`.
Remaining acting tools still need to read it. OpenHuman
`SecurityPolicy` remains the enforcement authority — the descriptor is the
carrier, not the policy.
`ParentExecutionContext` now also carries the descriptor as an ambient
fallback for internal/background fanout: the sub-agent runner scopes it into
child turns, and `AgentOrchestrationSession::spawn_agent` inherits it while
still letting explicit per-spawn descriptors win. Parent-context snapshots now
preserve an ambient descriptor when one is active, and post-turn session
memory extraction reuses the turn's parent snapshot instead of rebuilding a
default-only context after the scope has ended.
3. Emit `WorkspacePrepared/Violation/Cleanup` through the bridge; violations
also feed the security audit trail. Use 1.3.0
`WorkspaceDescriptor::enforce(path, events)` so the check and the
violation event are one call.
4. Sandbox descriptor: map `sandbox_mode` onto the descriptor's sandbox
field so sandboxed runs are inspectable.
## Deletions
- Later: `harness/worktree_context.rs` (74) task-local once shell/git and other
acting tools read the descriptor; per-tool ad hoc root plumbing.
## Acceptance
- Parallel edit-capable workers run in isolated worktrees with descriptor-
scoped tool roots; a path outside `allows()` is blocked AND evented;
worktree tests (235) green.
@@ -1,35 +0,0 @@
# 08 — Orchestration & graphs
Finish graph adoption: durable interrupts for approvals, workspace isolation
hooks, and the first per-agent `AgentGraph::Custom` runner. Parallel fanout,
`spawn_parallel_agents`, and graph-stub deletion already run on SDK graph
helpers / optional graph wiring.
Current status (2026-07-02): graph adapter internals such as the delegation
state machine and production delegation glue stay crate-internal; product
orchestration modules own the public RPC/tool surfaces and output shapes.
Target SDK surface: `graph::parallel::map_reduce` + `ParallelOptions`/
`FailurePolicy`/`ItemOutcome`, `Send`/`Command`/reducers/`ChannelSet`,
`Interrupt`/`ResumeTarget`/`Command::resume`, `WorkspaceIsolation`/
`WorkspaceDescriptor` + workspace events, `GraphTopology` export,
graph testkit.
Steps:
1. `01-map-reduce.md` — landed: `run_parallel_fanout` removed; callers use
the SDK helper directly.
2. `02-spawn-parallel-graph.md` — landed: spawn_parallel_agents as validate→
dispatch→worker→collect→finalize.
3. `03-interrupt-resume.md` — approval pauses as durable interrupts.
4. `04-graph-stubs.md` — default stubs deleted; land the first per-agent
bespoke `AgentGraph::Custom` runner.
5. `05-worktree-isolation.md` — worktrees behind `WorkspaceIsolation`.
Done when: every long-running orchestration has named nodes, topology export,
cancellation checks, the boilerplate stubs stay gone, and at least one
production agent owns a bespoke custom graph; fanout/parallel code paths stay
SDK-owned with deterministic order + failure policy.
Keep (product): workflow_runs/agent_teams/command_center product state + RPC
shapes; delegate/orchestration tool output formats; worktree policy.
@@ -1,48 +0,0 @@
# 09 — Embeddings & retrieval
Adapt OpenHuman embeddings/memory retrieval to crate interfaces; OpenHuman
stores stay authoritative.
Target SDK surface: `EmbeddingModel` trait, `VectorStore`, concrete `Retriever`,
`ScoredDoc`, `cosine_similarity`, `MemoryScope`, `AgentEvent::
{MemoryLoaded, MemorySaved}`.
## Steps
1. Adapter: implement crate `EmbeddingModel` over
`embeddings/provider_trait.rs::EmbeddingProvider` (voyage/openai/cohere/
ollama/cloud/noop impls unchanged underneath). Keep OpenHuman
`factory.rs`/rate-limit/retry as construction policy.
2. Retriever facade: adapt the memory search surface the harness uses
(`harness/memory_context.rs` recall injection,
`agent_memory::memory_loader`) to TinyAgents `EmbeddingModel`/`VectorStore`,
using the concrete crate `Retriever` where applicable and returning
`ScoredDoc`s; the harness loads retrieval context through the facade so
retrieval becomes swappable and event-visible (`MemoryLoaded`).
3. Memory source identity: preserve `metadata.path_scope` dedupe semantics
(CLAUDE.md rule) in the facade mapping.
4. Usage/cost: embedding calls emit crate `Usage` + catalog pricing (06.4).
5. Done: the vestigial `agent/memory_loader.rs` facade and unwired
`agent/tree_loader.rs` eager digest prefetch module were deleted; callers
use `agent_memory::memory_loader` directly.
6. Done: `embeddings::mod` no longer re-exports `memory_store::vectors`, and
`memory_search::vector::store` was deleted. Vector storage now has one
canonical owner (`memory_store::vectors`); `memory_search::vector` only owns
MMR/diversity selection.
## Deletions
- Done: `agent/memory_loader.rs` (tracked in `99-deletion-ledger.md`).
- Done: `agent/tree_loader.rs` (tracked in `99-deletion-ledger.md`).
- Done: `memory_search::vector::store` compatibility shim; direct callers import
`memory_store::vectors::cosine_similarity`.
## Acceptance
- Recall-injection parity on a scripted turn (same citations via
`collect_recall_citations`); embedding usage appears in cost records.
Keep (product): memory stores, retrieval ranking policy,
`memory_context_safety.rs` gating, archivist (orthogonal durable memory —
assessed KEEP 2026-06-30d; optional later: re-hang as `after_agent`
middleware without changing behavior).
@@ -1,60 +0,0 @@
# 10 — CapabilityRegistry projection
One policy-aware lookup over OpenHuman's many registries (agents, tools,
MCP, models, graphs).
Current status (2026-07-02): `agent.graph_topologies` still exposes the
structure-only graph snapshot as JSON-RPC, but the underlying
`tinyagents/topology.rs` helper/report surface is now crate-internal. A full
`CapabilityRegistry` projection remains pending.
Target SDK surface: `CapabilityRegistry` (`register_model`, `register_tool`,
`register_graph_blueprint`, `register_agent`, `register_descriptor`, `alias`,
`snapshot()`, `diagnostics()`, `names_including_aliases()`,
`to_model_registry()`, `to_tool_registry()`, `capability_resolver()`),
`ComponentId`/`ComponentKind`/`ComponentMetadata { id, kind, description, tags,
aliases }`, `RegistrySnapshot` (`to_dot()`),
`RegistryDiagnostic`/`DiagnosticSeverity`, `ModelCatalog`.
## Steps
1. Build-per-run (or cached) `CapabilityRegistry` projection in
`assemble_turn_harness`: the effective per-turn model/provider profile from
the assembled turn, tools from the turn's tool sets, graph descriptors from
the current topology reports
(delegation/scheduler/member/subagent-pipeline/spawn-parallel), agents from
both the runtime `AgentDefinitionRegistry` (`agent/harness/definition.rs`)
and the user-facing `openhuman::agent_registry` default/custom/config
entries. Wider model route/catalog projection happens earlier from config
and model routing, not inside `assemble_turn_harness` alone. `.rag`
`Blueprint` registration comes later when OpenHuman has real blueprint
artifacts; the current graph exports are structure snapshots, not compiled
blueprints. `AgentHarness` does not yet expose a registry-replacement setter,
so `to_model_registry()`/`to_tool_registry()` start as validation/projection
helpers before they can replace the live `register_model`/`register_tool`
blocks in `tinyagents/mod.rs`.
2. Diagnostics fail-closed pre-dispatch: duplicate tool names across
native/MCP/Composio/generated tools, unsafe aliases → registry
diagnostic errors (today: duplicate handling is scattered across generated
tools, MCP, and native registration instead of one SDK diagnostic stream).
TinyAgents 1.5.0 is pinned and exposes `AliasBinding`, alias diagnostics,
cross-kind name-reuse detection, and `ComponentKind::{Middleware,
Checkpointer, TaskStore, Listener}`; OpenHuman still needs to project those
SDK diagnostics into its runtime.
3. Introspection RPC: extend the existing `agent.graph_topologies` surface or
add a sibling RPC for `RegistrySnapshot` (JSON + DOT) so a UI/CLI can show
every active component.
4. This is also the enabler for `.rag` blueprints later (registry-bound
capability resolution) — out of scope for this wave, note only.
## Deletions
- Hand-rolled duplicate-name checks scattered in tool assembly (moved to
diagnostics). Do not delete the live registration glue in `tinyagents/mod.rs`
until `AgentHarness` can consume the projected registry directly.
## Acceptance
- Duplicate tool name → typed failure before model dispatch (test);
snapshot RPC lists models/tools/graphs/agents with metadata;
adapter-inventory test re-pointed at the registry.
@@ -1,93 +0,0 @@
# 11 — Testing & conformance (run last, per user preference)
Tests are deferred to the end of each workstream slice, with this final
consolidation pass.
## Parity matrix (crate route)
Chat turn, channel turn, sub-agent turn, unknown-tool recovery (RunPolicy),
approval denial, streaming text + reasoning + tool-arg deltas, early-exit
pause, model-call cap checkpoint, budget stop, fallback selection,
compression at 90% window, cache hit/miss, steering inject/cancel,
detached spawn→restart→wait, worktree-isolated parallel run.
Extend `src/openhuman/tinyagents/tests.rs`, `tests/agent_harness_e2e.rs`, and
`tests/agent_tool_loop_raw_coverage_e2e.rs`.
Current execution mode for this migration branch: tests are intentionally
deferred while code/docs are still moving quickly. Use docs drift and cargo
checks for small slices; run the behavioral suites in the final conformance
pass before declaring a workstream done.
## Conformance pass result (2026-07-02)
The deferred suites were re-enabled and run end-to-end after the adapter-first
migration slices landed. Outcome:
- The full test target **compiles** again (it had drifted out of compilation
while tests were deferred — `execute_tool_call`, `context::guard`,
`agent::cost`, `tree_loader` removals plus new
`workspace_descriptor`/`deterministic_cacheable`/capability params and the
`cache_creation_tokens`/`reasoning_tokens` `UsageInfo` fields).
- **Lib suite: green** — `13700 passed; 0 failed; 29 ignored` under the
canonical `RUST_MIN_STACK=16777216 … --test-threads=1` (the default
test-thread stack overflows the deep sub-agent dispatch future; the project
runner and CI pin the larger stack).
- **All integration/e2e binaries: green single-threaded** (parity matrix in
`agent_harness_e2e` / `agent_tool_loop_raw_coverage_e2e`, plus the raw
coverage and `json_rpc_e2e` suites).
- Two genuine regressions were caught and fixed during triage: (1)
`context_window_for_model` routed through `cost::catalog::lookup` began
shadowing precise pattern-table windows with rounded catalog rows — fixed
with a boundary-aware substring match and corrected `gpt-4.1{,-mini}` rows to
`1_047_576`; (2) the new same-family cross-route fallback masked terminal
provider errors in single-error test doubles — the doubles now fail on every
route so error propagation is still exercised.
- Behavioral changes intentionally introduced by the migration were re-baselined
in the assertions (unknown tools now recover through crate
`UnknownToolPolicy::ReturnToolError` emitting `unknown tool \`X\`` and bound at
the iteration cap rather than early-halting; multi-route model/middleware
inventory counts; `MAX_SPAWN_DEPTH` unified to 3; `session_db` RPC count;
`npm_exec` workspace-aware CWD).
Known **pre-existing** parallel-isolation flakes (green single-threaded, fail
only under `cargo test` default multi-threading because they mutate shared
process state — unrelated to this migration): `composio_set_api_key_validates_
candidate_key_even_when_stored_key_exists`, `resolved_daily_request_limit_
honors_env`, `resolve_sync_interval_honors_per_toolkit_env`,
`openai_codex_models_url_includes_client_version_query`,
`concurrent_acquire_grants_exactly_one_slot`, and the app_state/backend-
validation + `run_subagent_surfaces_provider_errors_and_can_be_cancelled`
timing tests. These need `serial_test`-style guards, tracked separately.
The mandatory deletion-ledger rows remain the follow-up: each gated deletion
(`reliable.rs`, `ThinkingForwarder`, `tool_filter.rs`/`tool_prep.rs`,
`worktree_context.rs`, `CostBudgetMiddleware`, `SqlRunLedgerCheckpointer`, the
session cutover set) can now proceed one row at a time against this green
baseline, re-running the relevant suite after each removal.
## Testkit adoption
Port legacy loop-wording assertions to crate testkit: `MockModel`
(`with_responses`, `with_tool_call`, `call_count`), graph `assert_graph`/
`GraphEventRecorder`/node doubles (`scripted_route_node`,
`interrupting_node`, `subagent_fake_node`), `RecordingListener` for event
assertions.
## Conformance
- Checkpointer: File vs Sqlite same interrupt/resume behavior (04.3).
- TaskStore: InMemory vs Jsonl lifecycle/filter/restart parity (07.2).
- Graph: `Send` fanout order, failure policy, recursion caps, resume
(08.x); fuzz-style small-graph composition if time allows.
## Known debts to clear here
- Previously flaky detached-orchestrator e2e timeout/hang debt (see 07.2 step
6): the tight 2s waits are now 15s, but re-run when tests are re-enabled
before treating that path as proven.
- `cost::init_global` is now idempotent/no-panic; keep future tests isolated
around process-global state so they do not depend on execution order.
- Coverage gate: ≥80% on changed lines applies when frontend/rust coverage
lanes are triggered. Docs-only migration-plan markdown does not trigger the
coverage lane, but code slices still need the normal changed-line coverage
before PR completion.
@@ -1,157 +0,0 @@
# 99 — Deletion ledger (master list)
Hard-migration means these files GO. Each row names its precondition step.
Rule: call-site search + parity coverage before every delete; tick rows as
they land.
## Immediately deletable (dead/vestigial — no precondition beyond call-site search)
- [x] `agent/memory_loader.rs` (5-line facade) — 09
- [x] `agent/tree_loader.rs` (210, unwired per #3170) — 09
- [x] `harness/compaction/mod.rs` shim — 03.1
## Superseded by existing middlewares (verify + delete)
- [x] `harness/compaction/cache_align.rs` (200) — 03.1/03.2
- [x] `tokenjuice::compact_tool_output` default-Full wrapper — 01.4
- [x] `context/microcompact.rs` (269) — 03.1
- [x] `context/pipeline.rs` (454) + `context/guard.rs` (236, keep stats structs) — 03.1
- [x] `context/tool_result_budget.rs` (172) — 03.1
- [x] `harness/payload_summarizer.rs` (490) — 01.4
- [x] `tinyagents/middleware.rs::MicrocompactMiddleware` struct + impl (~46) —
W2-microcompact (2026-07-03): upstreamed into the vendored crate as
`tinyagents::harness::middleware::MicrocompactMiddleware`
(`tinyhumansai/tinyagents@feat/microcompact-middleware`, gitlink bumped);
OpenHuman now constructs the crate type with `CLEARED_PLACEHOLDER` and
events off, so behavior is byte-identical. The in-house struct + impl are
deleted; the retained OpenHuman tests assert parity against the crate type.
Was the C3-corrected/C5 "extract-then-delete" item (the local microcompact
was NOT 1.5.0-superseded; this PR did the extraction).
## Deletable after SDK-surface adoption
- [x] `UNKNOWN_TOOL_SENTINEL` + `UnknownToolRewriteMiddleware` — 01.2
- [ ] crate-internal tool side-lookup in `tinyagents/middleware.rs` — 01.1
(live overlays for args-aware effects/permissions, CLI/RPC scope, and
generated runtime context until SDK policy metadata can represent them)
- [ ] `harness/tool_filter.rs` mechanics (299) +
`subagent_runner/tool_prep.rs` (344) — 01.3
(live until middleware owns child/toolkit selection; `tool_prep.rs` also
contains non-filter prompt helpers that must move first)
- [ ] `ThinkingForwarder` — 02.3
(streaming reasoning moved to `MessageDelta.reasoning`; tool-arg **argument**
fragments moved to `ToolCallDelta`/`MessageDelta.tool_call``emit_tool_args`
removed. Still live for the tool-call **start** marker `note_tool_call`
(crate `ToolDelta` has no `tool_name`) and the non-streaming reasoning
fallback; both must move before deletion)
- [ ] `inference/provider/reliable.rs` (now 900, was 1215 + 1443 tests) — 02.2
PARTIAL: the shared classifier/backoff exports (`is_non_retryable`,
`is_rate_limited`, `is_upstream_unhealthy`, `parse_retry_after_ms`,
`structured_http_4xx`, `compute_backoff`, etc.) + their unit tests were
extracted to `inference/provider/error_classify.rs`; cross-module importers
(model.rs, memory_tree, triage, config_rejection, channels) repointed;
classifier + reliable + triage + tinyagents suites green (138 passed).
RESIDUAL (the actual delete, deliberately NOT forced): `ReliableProvider`
is the universal retry/backoff/**model-fallback**/API-key-rotation/
**streaming-failover** wrapper applied to every provider in
`provider_factory.rs` and used by non-turn callers (memory-tree, channels)
that never enter the crate harness. The crate `FallbackPolicy` carries only
the hardcoded tier map, not user `config.reliability.model_fallbacks`, so
un-wrapping the turn path would silently drop user fallbacks, and a faithful
non-turn replacement reconstitutes most of the struct (net-zero) — the only
true removal is routing all non-turn provider calls through the crate
harness (large, and unverifiable by the mock suite, which never exercises
real transient-network retry). Its own header still says "do not delete
yet." Gated on that re-architecture.
- [x] `tinyagents/orchestration.rs::run_parallel_fanout` — 08.1
- [x] `harness/engine/` (entire dir, 309) — 05.2
- [ ] `agent/progress_tracing.rs` + tests (1338, if duplicate) — 05.2
(live web-progress exporter until journal-backed projection reaches parity)
- [ ] `harness/run_queue/` mechanics (317 total; 174 non-test) — 07.3
(live adapter for detached steer/collect plus web followup/parallel;
split before deleting)
- [ ] crate-internal `harness/spawn_depth_context.rs` (66) — 07.3
(live recursion guard for nested delegation)
- [x] `harness/worktree_context.rs` (74) — 08.5
(deleted: worktree-isolated workers now resolve CWD from the carried
`WorkspaceDescriptor` on `ToolExecutionContext` via
`effective_action_dir_for_context`; the subagent runner sets the descriptor
on the worker `RunContext` instead of the task-local. Parity tests
`shell_uses_workspace_descriptor_root_as_cwd` /
`git_resolves_cwd_from_workspace_descriptor` assert descriptor→worktree
root and no-descriptor→`security.action_dir`)
- [x] 32 × `agent_registry/agents/*/graph.rs` stubs (~420) + five default-only
non-registry graph modules — 08.4
- [x] `tinyagents/checkpoint.rs` (`SqlRunLedgerCheckpointer`, 250) — 04.3
(deleted: durable delegation graphs now checkpoint through the crate
`SqliteCheckpointer` at a dedicated `{workspace}/graph_checkpoints.db`.
Nothing outside the adapter read the old run-ledger `graph_checkpoints`
table, so the DDL was removed and pre-swap rows simply expire — orphaned
in-flight tasks are reconciled at boot per 07.2. Delegation + 08.3 durable
interrupt/resume + session_db suites green: 18 + 43 passed)
- [ ] crate-internal `CostBudgetMiddleware` (`tinyagents/middleware.rs`) +
crate-internal `agent/harness/turn_subagent_usage.rs` (176) task-local — 06
(live until crate budget/run-tree accounting avoids duplicate
`UsageRecorded` and covers parent-turn rollups)
W2-budget-dedupe (2026-07-03): dedupe guard landed — the event bridge now
records a model call's `UsageRecorded` exactly once, keyed on the run-scoped
iteration, so the observe-only crate `BudgetMiddleware`'s re-emit can't
double-count (`observability::OpenhumanEventBridge::record_usage`, `[budget]`).
Crate `BudgetMiddleware` installed OBSERVE-ONLY (empty `BudgetLimits`) at
`tinyagents/mod.rs`; local `CostBudgetMiddleware` demoted to a
divergence-logging shadow (`[budget_shadow]`, `after_agent`) but STILL
authoritative for enforcement. Flip criteria (must ALL hold before deleting
this row): (1) ≥ 500 parent+subagent turns with zero `[budget_shadow]`
divergence; (2) crate pricing table wired for money budgets; (3) run-tree
rollup via a shared `BudgetTracker` replacing the `turn_subagent_usage`
task-local. See the flip-criteria comment at the `tinyagents/mod.rs`
registration site.
- [ ] `agent/dispatcher.rs` (609) + `harness/parse.rs` (833) legacy tool-call
parsing — after XML/P-format transcripts read from the store and no
live path parses provider text (04.2 + verify)
(live compatibility shell for prompt dialect selection, history
serialization, XML/P-format fallback parsing, checkpoint cleanup, native
text fallback, and TinyAgents text-mode provider responses; trim
unused/test-only parse helpers before full delete)
## Deletable after session-store cutover (04.2 phase 4)
> **04.2 phase 2 landed (W2-shadow-reads, 2026-07-03):** a store-backed shadow
> reader runs beside the legacy transcript reader, normalizes both sides via
> `session_import/convert.rs`, and logs `[session_shadow_read]` divergence
> (compact, no-PII). Legacy stays authoritative; gated by
> `agent.session_shadow_reads` (default OFF) + `OPENHUMAN_SESSION_SHADOW_READS`
> kill switch. The rows below stay `[ ]` until the shadow logs are
> divergence-clean across the fixture matrix and reads are flipped to the store
> (phase 3), which is the precondition for these deletions.
- [ ] `session/transcript.rs` (1347) + tests (978)
- [ ] `session/migration.rs` (373) + tests
- [ ] `session/turn/session_io.rs` (391)
- [ ] `session_db/{ops,store,schemas,types}.rs` generic parts (~1.6k)
- [ ] `agent_orchestration/subagent_sessions/` (~650)
- [ ] `src/openhuman/session_import/` (~1.7k) + RPC controller — one
release after auto-import ships
## Shrink (not full delete)
- Deleted: `session/agent_tool_exec.rs` 471-line test-only parity shim — 01.4
- `session/turn/tools.rs` 697 → parent-context/assembly glue — 01.3
(dynamic delegation refresh and skill-event catalogue reconciliation remain
live until middleware owns contextual selection)
- `subagent_runner/ops/*` 2764 (+1827 companion tests) → graph nodes/tests — 07.1
- `running_subagents.rs` 1250 → ≤~300 policy/executor glue — 07.2
- `tools/spawn_parallel_agents.rs` is a thin tool shell; remaining shrink
target is `agent_orchestration/spawn_parallel_graph.rs` graph mechanics
(1280) — 08.2
- `context/` → stats + product prompt state — 03
- `cost/catalog.rs` (622) → catalog snapshot loader once config seeding, cost
estimates, and `context_window_for_model` all read one catalog projection —
02.4
## Never delete (product policy)
Prompts/`agent/prompts/`, agent registry definitions, security/approval
semantics, credentials/factory/router names, triage, task board/dispatcher,
archivist, memory stores, worktree policy, `host_runtime.rs`, multimodal
policy, JSON-RPC shapes, `DomainEvent` until all subscribers move.
@@ -1,103 +0,0 @@
# C2b — Task board / todos onto `graph::todos` (parity note)
Status: **first slice landed** (branch `feat/tinyagents-c2-todos`). Adapter-first,
shadow-only. Legacy stays authoritative; nothing here changes product behavior.
This note pairs with the CONTINUATION plan §C2 (step 3) and records what the
crate `tinyagents::graph::todos` surface maps onto in OpenHuman, and what it does
**not** — the residue that must stay in the product host after a future cutover.
## What landed in this slice
- `src/openhuman/todos/graph_shadow.rs` — the adapter:
- Total, lossless status mapping OpenHuman ↔ crate (`map_status_to_crate` /
`map_status_from_crate`) — the two `TaskCardStatus` enums share the same
seven variants (`Todo`, `AwaitingApproval`, `Ready`, `InProgress`,
`Blocked`, `Done`, `Rejected`).
- `TaskBoardCard` field-by-field conversion (`to_crate_card`) — all metadata
(objective, plan, assignedAgent, allowedTools, approvalMode,
acceptanceCriteria, evidence, notes, blocker, sessionThreadId,
sourceMetadata, order, updatedAt) preserved.
- `spawn_mirror` — after every authoritative `todos::ops::save_cards` write of
a `Thread` board, mirrors the persisted cards into a crate `FileStore`
(`<workspace>/tinyagents_graph_store`) under namespace `graph.todos`. Fire-
and-forget, log-only; a crate rejection (e.g. the single-`InProgress`
invariant) is warn-logged as a **DIVERGENCE**.
- `spawn_shadow_claim` — wired into `todos::ops::claim_card` (which is the
single claim entry-point the dispatcher's two claim sites in
`task_dispatcher/dispatch.rs` funnel through, plus RPC/reclaim callers). It
seeds the crate board with the pre-claim snapshot and replays the crate
`claim_card` CAS, warn-logging when the crate ok/err verdict disagrees with
the authoritative legacy claim.
The legacy claim/save path is byte-for-byte preserved: `claim_card` was
refactored to compute one ok/err verdict via an extracted `apply_claim` helper
(so the shadow sees the same not-found / wrong-status / invariant outcomes), but
the persisted result and all existing tests are unchanged.
## Crate `TodoTool` vs `agent/tools/todo.rs` — what maps
The crate ships a single multiplexer `TodoTool` (`op`-dispatched) that is a near
drop-in for the OpenHuman `todo` tool. Shared surface:
| Concern | Crate `TodoTool` | OpenHuman `tools/todo.rs` | Match? |
| --- | --- | --- | --- |
| Dispatch style | single tool, `op` field | single tool, `op` field | yes |
| Thread binding | `ToolExecutionContext::thread_id` (never an arg) | `thread_context::current_thread_id()` + `fork_context` parent | yes (both bind to current thread, never an arg) |
| `add`/`edit`/`update_status`/`remove`/`replace`/`clear`/`list` | present | present | yes |
| Optional card fields | objective/plan/assignedAgent/allowedTools/approvalMode/acceptanceCriteria/evidence/notes/blocker | same | yes |
| Return shape | `{ threadId, cards, markdown }` | `{ threadId, cards, markdown }` | yes |
| Status aliases | `parse_status` (pending→todo, approved→ready, …) | `ops::parse_status` (identical alias table) | yes |
| Single-`InProgress` invariant | `enforce_single_in_progress` (hard error) | `enforce_single_in_progress` (identical) | yes |
| `claim_card` CAS | `store::claim_card(expected, target)` | `ops::claim_card(expected, target)` | yes (identical semantics; proven by shadow tests) |
## What does NOT map (product residue — must stay in the host)
1. **Approval-gate coupling.** OpenHuman's `todo` tool stamps a default
`approvalMode` by reading `config.autonomy.require_task_plan_approval`
(`default_task_approval_mode`), and the dispatcher's
`requires_plan_approval` + `TaskPlanAwaitingApproval` `DomainEvent` drive the
interactive plan-review gate. The crate `TodoTool` has **no** config read and
**no** approval-gate wiring — it exposes `decide_plan`/`revise_plan` state
transitions only. The gate policy stays product.
2. **`DomainEvent` emissions.** `ops::claim_card`/mutations emit
`AgentProgress::TaskBoardUpdated` (via `fork_context` `on_progress`) and the
dispatcher publishes `TaskPlanAwaitingApproval`. The crate store emits
nothing. All event vocabulary stays product (ledger: keep).
3. **RPC projection shapes.** `threads.task_board_*` and `openhuman.todos_*`
(see `todos/schemas.rs`) are the wire contracts the kanban UI binds to
(`app/src/services/api/todosApi.ts`, `USER_TASKS_THREAD_ID = "user-tasks"`).
These are **unchanged** by this slice and, per §C2, become read-side
projections over the crate store only at cutover — not now.
4. **Scratch board.** OpenHuman has a thread-less in-memory `BoardLocation::Scratch`
fallback (tool calls outside a chat thread). The crate board is always
`(Store, thread_id)`, so scratch mutations have **no** crate mirror target —
the shadow skips them (trace-logged).
5. **Persistence substrate + timestamps.** Product persists RFC3339
`updated_at` to `<workspace>/agent_task_boards/<hex(thread_id)>.json`; the
crate uses epoch-millis strings in a `Store` namespace. The mirror does not
reconcile timestamps (cosmetic). Card-id minting also differs
(`task-<uuid>` product vs `task-<seq>` crate) but ids are passed through, so a
persisted board round-trips.
6. **Run lifecycle.** `todos/runs.rs` (run records, heartbeats, stale-reclaim)
and `task_dispatcher/` executor mechanics (executor resolution, autonomous
run, board write-back) are **not** part of the crate todos surface — they
stay product and are the §C2 step-3 "runner node" work, tracked separately.
## Single-writer constraint
The crate `Store` has no compare-and-set, so ns `graph.todos` assumes a single
writer. The core process is that single writer (both the mirror and the
shadow-claim run in-core). Documented in the module header; honoured because all
mutations funnel through `todos::ops`.
## Next (not in this slice)
- Flip the mirror from shadow to authoritative (crate store becomes the source
of truth; legacy JSON becomes a projection or is retired).
- Reimplement `threads.task_board_*` / `openhuman.todos_*` as projections over
the crate store.
- Replace the dispatcher claim/poll loop with the crate `claim_card` CAS + a
graph runner node, keeping `DomainEvent` emission + channel bindings product.
- Delete `task_board.rs` + todo CRUD mechanics + dispatcher executor mechanics
(~3.2k + tests) once parity logs are clean.
@@ -1,265 +0,0 @@
# TinyAgents Migration — Continuation Plan (2026-07-03)
Status: supersedes the ordering in `README.md` for remaining work. Written
after a ground-truth audit of `main` (post-#4249), a re-inventory of the
TinyAgents crate (1.4.0 published, 1.5.0 tagged), and a critical
re-evaluation of the `99-deletion-ledger.md` "Never delete" list.
## 1. Where we actually are (ground truth, main @ 2026-07-03)
The "finish TinyAgents harness migration" PR (#4249#4399) has landed.
Per-workstream state:
| Workstream | State |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 00 baseline | Done (1.3.0 + sqlite, rusqlite 0.40) — **needs re-bump to 1.4/1.5** |
| 01 tooling | Mostly done. Live: crate-internal tool side-lookup (01.1), `tool_filter.rs` 299 + `tool_prep.rs` 344 (01.3) |
| 02 models | Mostly done. Live: `reliable.rs` 900 (gated on re-architecture), `ThinkingForwarder` residual seams |
| 03 context/cache | Done. `context/` is 1.3k of product prompt/stats state |
| 04 sessions | **Primary unfinished work.** Live path is 100% legacy: `transcript.rs` 1347, `migration.rs` 373, `session_io.rs` 463, `session_db/` 4.5k, `subagent_sessions/` 653, `session_import/` 1.9k. Crate `Store`/`AppendStore` used only by the write-only importer. 04.3 checkpointer swap IS done in code (`tinyagents/checkpoint.rs` deleted) — the 04.3 doc text is stale |
| 05 events | 05.2 engine deletion done. `progress_tracing` (817 + 477 langfuse + 719 tests) still the live web-progress exporter; journal projection not at parity |
| 06 cost | Not wired: no `BudgetMiddleware`/`BudgetLimits`/`RunTree` in the shared runner. Blocker: `UsageRecorded` de-duplication |
| 07 subagents | Diagnostic-skeleton graph only; procedural runner still live. `running_subagents.rs` has **grown to 1931 lines** (target ≤300); `JsonlTaskStore` adopted for detached lifecycle records; `run_queue/` + `spawn_depth_context.rs` still live |
| 08 orchestration | 08.1/08.2/08.4 done. Live: 08.3 durable interrupts for approvals, 08.5 `worktree_context.rs` fallback thread |
| 09 embeddings | Done |
| 10 registry | Pending — `CapabilityRegistry` unused in src/ |
| 11 testing | Conformance pass green on 2026-07-02 baseline |
Crate APIs available but **unused** in src/: `JsonlTaskStore` (now partially
adopted), `BudgetMiddleware`, `ContextualToolSelectionMiddleware` (only its
shadow), `UnknownToolPolicy`, `CapabilityRegistry`.
## 2. Crate delta: 1.4.0 / 1.5.0 (the upgrade this plan targets)
- **1.4.0 (published 2026-07-02)**
- `graph::goals` — durable per-thread `ThreadGoal`: completion contract,
token budget, Active/Paused/BudgetLimited/Complete, `goal_gate_node`
self-driving loop, `run_continuation_tick`, `note_user_turn`, model tools
`goal_get/goal_set/goal_complete`, host `goal_pause/resume/clear`.
Persists on harness `Store` ns `graph.goals`.
- `graph::todos``TaskBoard` kanban (Todo→Ready→InProgress→Done, Blocked,
AwaitingApproval), single-`InProgress` invariant, `claim_card` CAS,
single multiplexer `TodoTool`. Persists ns `graph.todos`.
- Graph resilience: `CompiledGraph::with_node_retry(RetryPolicy)`,
opt-in backoff sleeping, failure-boundary checkpoints +
`CompiledGraph::retry(thread)` resumable failures,
`GraphEvent::NodeRetryScheduled`.
- **1.5.0 (tagged 2026-07-03, not yet on crates.io)**
- `harness::no_progress::NoProgressTracker` — extracted from OpenHuman
PR #4389. Pure state machine: `record(step, &ToolAttempt) ->
Continue/Nudge/Halt`; identical-failure ladder, varied-failure backstop,
fast-trip on hard policy rejects.
- Known crate limitation relevant here: `Store` has no compare-and-set, so
goals/todos mutations must funnel through one process (fine — the core is
the single writer).
## 3. Re-evaluated component verdicts (revises `99-deletion-ledger.md`)
The old "Never delete" list over-protects. Revised verdicts, ordered by
reclaimable lines (non-test; tests roughly double each figure):
### Migrate to crate, then delete locally (upstream-extraction candidates)
Precedent: `NoProgressTracker` was extracted upstream from #4389 and shipped
in 1.5.0. Same play for:
| Component | Lines (~generic) | Notes |
| ------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pformat.rs` + `dispatcher.rs` + `harness/parse.rs` | 1941 (~1900) | Tool-call dialect machinery (P-format encoder, permissive XML/JSON parser with key-drift recovery). Zero DomainEvent coupling. Delete gate: 04.2 read-cutover (no live path parses provider text) |
| `multimodal.rs` | 1690 (~1550) | `[IMAGE:]`/`[FILE:]` marker → provider content blocks, mime allowlist, PDF extraction, fetch gating, truncation budget. Only the marker convention is product |
| `progress_tracing.rs` + `langfuse.rs` | 1294 (~1200) | Crate already ships Langfuse exporters + journals. Delete gate: 05.3 journal-backed web-progress parity |
| `tool_result_artifacts/` | 588 (~500) | Already built on crate `Store`; overflow-to-artifact is a generic harness concern. Product residue: PII scrub hook |
| `hooks.rs` + `stop_hooks.rs` trait machinery | 543 (~450) | PostTurnHook/StopHook traits are pure harness; product hook bodies stay |
| `host_runtime.rs` adapter core | 456 (~350) | Native/Docker `RuntimeAdapter` overlaps crate workspace isolation |
| `ArgRecoveryMiddleware`, `RepeatedToolFailureMiddleware` core | ~300 | Latter becomes a thin driver over crate `NoProgressTracker` (1.5.0) |
| `tool_filter.rs` fuzzy ranker | 299 (~250) | Generic ranking; Composio input types stay product |
| `triage/` evaluator+routing+decision core | ~800 of 3779 | Generic "LLM triage node" (tiered fallback, cache, verdict parse). `envelope.rs`/`escalation.rs`/`events.rs` stay product (Composio, DomainEvents, agent ids) |
### Replace with crate features, then delete (no upstreaming needed)
| Component | Lines | Replaced by |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `thread_goals/` | 2185 | `graph::goals` (1.4.0). OpenHuman keeps RPC schemas + UI projection over the crate store |
| `agent/task_board.rs` + `tools/todo.rs` mechanics | 604 + ~430 | `graph::todos` `TaskBoard` + `TodoTool` (1.4.0). RPC shapes (`threads.task_board_*`, `openhuman.todos_*`) become projections |
| `task_dispatcher/` executor mechanics | ~450 of 1494 | `claim_card` CAS + graph runner node; DomainEvent emission + channel bindings stay |
| 6 duplicate middlewares in `tinyagents/middleware.rs` (2448 total): `OpenHumanToolExposureShadow`, `CacheAlign`, `Microcompact`, `CostBudget`, `PromptCacheSegment` (partial), `ToolPolicyMiddleware` (local) | ~900 | Crate `ContextualToolSelectionMiddleware`, cache guard, compaction, `BudgetMiddleware`, `ToolAllowlistMiddleware`. All are self-acknowledged shadows/parity holds |
| `spawn_depth_context.rs` | 92 | Crate `RecursionPolicy`/`RecursionStack` |
| legacy session stack (04.2 phase 4) | ~9000 incl. tests | Crate `Store`/`AppendStore`/`StoreChatHistory` |
### Confirmed product — keep (ledger was right)
`archivist/` (memory/learning policy — only the hook-scheduling shell ~200
lines is generic), `bus.rs`, `schemas.rs`, `error.rs`, `turn_origin.rs`,
`memory_context.rs`, `debug/`, `library/`, `progress.rs` event vocabulary,
prompts **content** (the section/render machinery ~600 lines is generic but
decoupling is low-value), preference/`run_workflow`/`delegate_to_personality`
tools, approval/`MemoryProtocol`/`CliRpcOnly`/`ToolOutcomeCapture`
middlewares.
Cross-cutting gate for the coupled middle tier (session turn/builder, triage
escalation, task dispatch): **DomainEvent subscribers and JSON-RPC shapes**
these must become projections before their hosts can shrink.
### Coverage sweep: harness files previously unreferenced by any plan doc (2026-07-03)
A basename grep of `agent/harness/**` against this plan folder found these
non-test files with no verdict anywhere. Assigned now (tests follow parents):
| File | Lines | Verdict |
| --- | --- | --- |
| `subagent_runner/extract_tool.rs` | 612 | **[migrate/delete]** — generic progressive-disclosure Q&A over handoff-cached payloads; pairs with `handoff.rs`. Goes with the C5 overflow-to-artifact/handoff extraction |
| `subagent_runner/handoff.rs` | 287 | **[migrate/delete]** — generic oversized-result handoff cache (crate tool-output/artifact overlap). C5 |
| `subagent_runner/types.rs` | 232 | **[shrink]** — spawn options/outcome/error taxonomy largely maps to crate `SubAgentPolicy`/`OrchestrationTaskKind`. C6 |
| `subagent_runner/autonomous.rs` | 29 | **[keep]** — approval-gating override policy for unattended skill runs |
| `fork_context.rs` | 226 | **[delete-via-crate]** — task-local parent-context carrier working around the `Tool` trait; replace with `ToolExecutionContext`/`RunContext` fields (same play that deleted `worktree_context.rs`). C6 |
| `sandbox_context.rs` | 94 | **[delete-via-crate]** — same task-local pattern (sandbox_mode carrier). C6 |
| `task_recency_context.rs` | 106 | **[delete-via-crate]** — same pattern (Composio recency window). C6 |
| `turn_attachments_context.rs` | 71 | **[delete-via-crate]** — same pattern (vision-attachment forwarding). C6 |
| `session/turn_checkpoint.rs` | 105 | **[delete]** with the C1 session cutover (turn checkpointing rides crate checkpoints) |
| `memory_protocol.rs` | 386 | **[keep]** — product memory-protocol state machine (#4116) behind `MemoryProtocolMiddleware` |
| `builtin_definitions.rs` | 273 | **[keep]** — product agent-definition data facade |
| `definition_loader.rs` | 295 | **[keep]** — product TOML definition loader |
| `archivist/{hook_impl,recap,tree_ingest}.rs` | 592 | **[keep]** — archivist internals, verdict inherited from §3 |
The four task-local context carriers (`fork_context`, `sandbox_context`,
`task_recency_context`, `turn_attachments_context`, ~500 lines) are one
C6 work item: extend the crate execution context instead of task-locals.
## 4. Continuation workstreams (execution order)
Sized for `/goal` execution like the original folders; each step lands code +
tests + deletions + a ledger tick.
### C0 — Crate bump to 1.4 (then 1.5 when published)
1. Bump both Cargo worlds to `tinyagents = "1.4"`; re-verify sqlite chain.
2. When 1.5.0 publishes: bump again; rewrite `RepeatedToolFailureMiddleware`
as a driver over `harness::no_progress::NoProgressTracker`; delete the
in-house identical-failure ladder.
3. Adopt `with_node_retry` + `CompiledGraph::retry` on the delegation and
spawn-parallel graphs (replaces bespoke retry glue; complements 08.3).
4. Update `00-baseline.md` "1.3.0 delta" → 1.4/1.5 delta; refresh
`docs/tinyagents-sdk-gaps.md` (goals/todos and no-progress close two
OpenHuman-convergence items).
### C1 — Sessions cutover (04.2 phases 24) — biggest single unlock
The importer (`session_import/`) already proves shape parity. Execute:
1. 04.1 live dual-writes (turns append `session.{stem}.messages` + descriptor
upsert alongside legacy JSONL).
2. Shadow reads + parity fixtures (11-fixture matrix), then flip reads.
3. Retire: `transcript.rs` (1347+978), `migration.rs` (373+170),
`session_io.rs` (463), `session_db/` generic parts (~1.6k),
`subagent_sessions/` (653); `session_import/` one release later.
4. Then (unblocked by "no live path parses provider text"): delete
`dispatcher.rs` + `parse.rs` + `pformat.rs` (~1.9k + tests), after
upstreaming the dialect machinery (C5) or accepting native-only.
~11k lines total.
5. Fix the stale 04.3 doc (checkpointer swap already landed; `checkpoint.rs`
is gone).
### C2 — Thread goals + thread tasks onto `graph::goals`/`graph::todos`
1. Adapter: `thread_goals/store.rs`+`runtime.rs`+`continuation.rs`
crate `ThreadGoal` + `goal_gate_node` + `run_continuation_tick`; keep
`thread_goals/schemas.rs` RPC shapes as projections; map
`goal_get/set/complete` model tools + host pause/resume/clear.
2. One-time migration of existing goal rows into ns `graph.goals`.
3. `task_board.rs` → crate `TaskBoard`; `tools/todo.rs` → crate `TodoTool`;
`task_dispatcher/` claim/poll loop → `claim_card` CAS + runner node.
DomainEvent emissions + `threads.task_board_*`/`openhuman.todos_*` RPC
become read-side projections of the crate store.
4. Single-writer constraint honoured (core is the only mutator; document it).
5. Delete: `thread_goals/{store,runtime,continuation}.rs` mechanics,
`task_board.rs`, todo CRUD mechanics, dispatcher executor mechanics
(~3.2k + tests).
### C3 — Middleware de-duplication (01.1/01.3/06 finish)
1. Flip `ContextualToolSelectionMiddleware` from shadow to owner (parity logs
are already accumulating); delete `OpenHumanToolExposureShadowMiddleware`,
then `tool_filter.rs` mechanics + `tool_prep.rs` selection half.
2. De-duplicate `UsageRecorded` (single owner: crate event → bridge records
once), then install crate `BudgetMiddleware`/`BudgetLimits`; delete local
`CostBudgetMiddleware` + `turn_subagent_usage.rs` task-local (206) in
favour of `RunTree` rollup.
3. ~~Delete `CacheAlign` + `Microcompact`~~ CORRECTED by C3 execution
(2026-07-03): `CacheAlign` deleted (warn-only, crate cache guard already
installed — commit on `feat/tinyagents-c3-middleware-dedupe`).
`Microcompact` is NOT crate-superseded — tinyagents 1.5.0 has no
tool-result body-clearing equivalent and the local one is live on the
session turn path — moved to the C5 upstream-extraction batch (extract a
crate microcompact first, then delete locally).
4. ~~Adopt `UnknownToolPolicy::Rewrite`~~ CORRECTED by C3 execution: the
sentinel + `UnknownToolRewriteMiddleware` were already deleted in 01.2;
live policy is `UnknownToolPolicy::ReturnToolError`, which preserves the
attempted tool name + args (#4419 UX). Rewrite mode would regress (needs a
catch-all target tool); rationale comment added at `run_policy_for`. Done —
no further action.
Net: `tinyagents/middleware.rs` 2448 → ~1500 (Microcompact stays until C5).
### C4 — Events/progress projection (05.3) then delete progress_tracing
1. Journal-backed web-progress projection (`HarnessEventJournal` +
`HarnessStatusStore` + late-attach `replay_from`).
2. Parity vs `SpanCollector` spans; then delete `progress_tracing.rs` +
`progress_tracing/` (~2k incl. tests) — Langfuse rides crate exporters.
3. DomainEvent/AgentProgress become projections (ledger final clause).
### C5 — Upstream extraction batch (tinyagents PRs, then local deletes)
In crate-repo PRs, mirroring the NoProgressTracker precedent, in value order:
1. `multimodal` attachment resolver (~1550) — marker convention stays here.
2. Tool-call dialect layer (`pformat`/parser) (~1900) — enables C1 step 4 to
be a pure delete.
3. Overflow-to-artifact tool-result store (~500).
4. PostTurnHook/StopHook trait machinery (~450); host runtime adapter (~350);
fuzzy tool ranker (~250); ArgRecovery (~150); triage evaluator core
(~800) as a generic "LLM triage node".
Each: crate PR → bump → local adapter shrinks to product residue → delete.
### C6 — Subagents finish (07)
1. Absorb `ops/runner.rs` (1212) + `ops/graph.rs` (1039) into named pipeline
nodes (07.1); procedural runner retired.
2. `running_subagents.rs` 1931 → ≤300: status/tombstone persistence fully on
`JsonlTaskStore` + `orchestrate_*` tools (07.2).
3. Steering: crate `SteeringRegistry` owns lanes; split then delete
`run_queue/` (317); `spawn_depth_context.rs``RecursionPolicy` (07.3).
4. Replace the four task-local context carriers (`fork_context`,
`sandbox_context`, `task_recency_context`, `turn_attachments_context`,
~500 lines) with typed fields on the crate execution context (see §3
coverage sweep); shrink `subagent_runner/types.rs` onto crate
`SubAgentPolicy`/task types.
### C7 — Remaining gated items
- 08.3 durable approval interrupts (now easier with 1.4.0 resumable
failures); 08.5 drop the `worktree_action_dir` fallback thread, delete
`worktree_context.rs` remnant wiring.
- 10 CapabilityRegistry projection + fail-closed diagnostics.
- 02.2 `reliable.rs`: unchanged verdict — gated on routing non-turn provider
calls through the crate harness; do not force.
- `ThinkingForwarder`: delete when crate `ModelDelta` grows reasoning +
tool-name-on-start (file upstream issue; sdk-gaps §3).
## 5. Expected reclaim
| Phase | Local lines deleted (incl. tests, rough) |
| --------------------- | ---------------------------------------------------------------------------- |
| C1 sessions + dialect | ~11,000 |
| C2 goals/todos | ~4,000 |
| C3 middleware dedupe | ~1,500 |
| C4 progress tracing | ~2,000 |
| C5 upstream batch | ~6,000 |
| C6 subagents | ~4,500 |
| Total | **~29,000** (of ~57k in `agent/` + ~11k adapter + ~9k session/goals modules) |
## 6. Rules (unchanged)
Approval/security/sandbox/credential boundaries inviolate; JSON-RPC contracts
stable unless a migration note lands; adapter → proven parity → delete;
explicit `git add <paths>`; verify branch before commit. Tests deferred per
workstream slice with a final conformance pass (11).
@@ -1,170 +0,0 @@
# Handoff — TinyAgents migration wave 1 (2026-07-03)
Context file for picking this work up in a fresh session. Read together with
[`CONTINUATION-2026-07.md`](CONTINUATION-2026-07.md) (the plan this wave
executes) and [`99-deletion-ledger.md`](99-deletion-ledger.md).
## What this session did
1. **Audit** — ground-truth audit of post-#4249 main, TinyAgents crate
re-inventory (1.4.0 published: `graph::goals`/`graph::todos`/graph
resilience; 1.5.0: `NoProgressTracker` extracted from our #4389), and a
critical re-review of the deletion ledger's "never delete" list. Product of
that: `CONTINUATION-2026-07.md` (workstreams C0C7, ~29k-line reclaim) with
a coverage-sweep appendix for previously unreferenced harness files.
2. **Vendored the SDK**`vendor/tinyagents` git submodule pinned at tag
`v1.5.0`; `[patch.crates-io] tinyagents = { path = ... }` in BOTH Cargo
worlds (root `Cargo.toml`, `app/src-tauri/Cargo.toml`); Dockerfile now
`COPY vendor/ vendor/`; release-production/staging docker jobs run a
targeted `git submodule update --init vendor/tinyagents`. All other
cargo-running CI jobs already checkout `submodules: recursive`; the mobile
crates (`app/src-tauri-mobile`) do NOT depend on the core crate, so their
`submodules: false` stays. Agents can now edit SDK source in-tree and PR
upstream from the submodule (do this in C5).
3. **Executed wave 1** via a 6-agent workflow (run id `wf_df08984c-139`,
scripts under the session dir; all agents completed).
4. `agent-diff-v0.58.7-to-HEAD.diff` at repo root (gitignored) — full diff of
`src/openhuman/agent/` since the last release, for review.
## Branch map (local; base → children)
- **`feat/tinyagents-c0-15-baseline`** ← base for everything below; branched
from `docs/tinyagents-migration-continuation` (which holds the plan docs and
branched from upstream/main).
- `c7f287380` bump tinyagents 1.5.0 (+ .diff gitignore)
- `e2f010cba` vendor submodule + [patch.crates-io] both worlds
- `4c2895801` RepeatedToolFailureMiddleware → crate `NoProgressTracker`
driver (in-house identical/varied/hard-reject ladder deleted; Nudge→
`SteeringCommand::Redirect`, Halt→`HaltSummarySlot`+Pause+reset; 25
middleware tests green)
- `fbafad2d3` CI docker submodule fixes
- `dd63b3a66` `with_node_retry(RetryPolicy::default().with_max_attempts(1))`
on delegation + spawn-parallel graphs (behavior-preserving seam; raise
attempts later; backoff sleeping off)
- `02be67b06` plan corrections from C3 (below)
- **`feat/tinyagents-c1-dual-writes`** — 04.1 done. Live turns dual-write to
`{workspace}/tinyagents_store/{kv,journal}` (`session.{stem}.messages` +
descriptor upsert, via `session_import/convert.rs` normalization). New
config flag `agent.session_dual_write` (serde default **ON**);
`OPENHUMAN_SESSION_DUAL_WRITE` is a pure **kill switch** (can force off,
never on), read per-turn. Parity test:
`session_import::live_tests` (2 pass). Read path untouched.
- **`feat/tinyagents-c2-goals`** — `thread_goals/crate_adapter.rs` dual-write
mirror into ns `graph.goals` (keys byte-identical to the crate reader,
proven by reading back via `tinyagents::graph::goals::store::get`);
idempotent `migrate_legacy_goals_into_crate_store` (callable, NOT wired to
boot); shadow tool surface flag-gated **OFF**. Legacy store authoritative.
- **`feat/tinyagents-c2-todos`** — `todos/graph_shadow.rs` mirrors boards into
ns `graph.todos` at `<workspace>/tinyagents_graph_store`; `claim_card` CAS
shadow through the single refactored `todos::ops::claim_card` chokepoint
(`apply_claim` helper), divergence warn-logged; parity note
`C2b-todos-parity.md` (what maps vs product residue). Legacy authoritative.
- **`feat/tinyagents-c3-middleware-dedupe`** — PARTIAL by design:
`CacheAlignMiddleware` deleted (147 lines; crate cache guard already
installed). See "plan corrections" for the two refusals.
- **`feat/tinyagents-c4-journals`** — restart-stable event ids
(`EventSink::with_stream_id(run_id)``{run_id}-evt-{offset}`), run id
minted once via `journal::mint_run_id`; `FileStatusStore` records thread_id
(`HarnessRunStatus::with_thread`) so `list_by_thread` answers; late-attach
replay test reconstructs from the store alone.
All slice branches were created FROM `feat/tinyagents-c0-15-baseline`
(worktree HEADs were stale; agents rebased themselves correctly).
## Plan corrections discovered during execution (already committed, 02be67b06)
- **MicrocompactMiddleware is NOT crate-superseded** — tinyagents 1.5.0 has no
tool-result body-clearing; local one is live at `turn/core.rs` (~:885,
keep_recent=5). Moved to C5: extract into the crate first, then delete.
- **Unknown-tool is already done** — sentinel + rewrite middleware deleted in
01.2; live policy `UnknownToolPolicy::ReturnToolError` deliberately kept
over `Rewrite` (preserves #4419 attempted-name UX; Rewrite needs a catch-all
target tool). Rationale comment at `run_policy_for`.
- C3's shadow-parity investigation: exposure-shadow parity logs exist but are
not yet asserted divergence-free — flipping
`ContextualToolSelectionMiddleware` to owner still needs a parity-log audit.
## Known debt / gotchas for the next session
- **Worktrees + submodule**: fresh worktrees leave `vendor/tinyagents` empty →
cargo fails on the patch path. Always
`git submodule update --init vendor/tinyagents` first.
- Test invocation: `RUST_MIN_STACK=16777216 … --test-threads=1`
(deep sub-agent futures overflow default stacks); `GGML_NATIVE=OFF` for
cargo on Apple Silicon. Full suites were deliberately NOT run — targeted
module tests only (user directive). Integration debt is listed per-slice in
the workflow reports and belongs to the final conformance pass (plan §11).
- C1's `RunContext.stores` session-KV registration has no in-run consumer
until 04.2 (`StoreChatHistory` adoption was deferred as read-path work).
- C4 follow-ups: replay RPC (`agent.run_events`) unexposed;
parent/root run-id lineage still 05.2/05.3.
- The five slice branches will conflict lightly in `tinyagents/middleware.rs`
/ `mod.rs` when merged together — merge C0 first, then slices one at a
time, resolving against C0's tree.
## Wave 1 (merged)
All five wave-1 slice branches were MERGED into
`feat/tinyagents-c0-15-baseline` and pushed to PR **#4473**, which landed the
entire wave-1 scope into `tinyhumansai/openhuman:main`.
## Wave 2 status (2026-07-04) — DONE, on `feat/tinyagents-wave2`
All four wave-2 slices are implemented and integrated onto ONE branch
`feat/tinyagents-wave2` (off `upstream/main`), one focused commit each, and
opened as a single combined PR. Adapter-first throughout: legacy authoritative,
crate features shadow + log divergence, deletions gated on proven parity.
1. `W2-microcompact-upstream`**done.** The generic
`MicrocompactMiddleware` (caller-supplied placeholder, opt-in `Compressed`
event, idempotent tool-body clearing) was implemented IN the vendored crate
and pushed to `tinyhumansai/tinyagents@feat/microcompact-middleware`
(commit `ac73382`) BEFORE the gitlink bump. OpenHuman swapped to the crate
type (constructed with `CLEARED_PLACEHOLDER`, events off → byte-identical);
local struct+impl deleted, OpenHuman tests retargeted as the parity contract.
Crate tests: 5 green. Ledger row ticked.
2. `W2-shadow-reads`**done.** 04.2 phase 2 store-backed shadow reader beside
the legacy transcript reader; `[session_shadow_read]` divergence logging;
flag `agent.session_shadow_reads` (default OFF) + `OPENHUMAN_SESSION_SHADOW_READS`
kill switch. Legacy authoritative. (Flip → reads-from-store is phase 3, the
~9k-line deletion unlock.)
3. `W2-budget-dedupe`**done.** Event bridge records each model call's usage
exactly once (dedupe guard keyed on the run-scoped iteration cursor); crate
`BudgetMiddleware` installed observe-only (empty `BudgetLimits`); local
`CostBudgetMiddleware` demoted to a `[budget_shadow]` divergence logger, still
authoritative for enforcement. Three flip criteria documented at the
`tinyagents/mod.rs` registration site + in the ledger.
4. `W2-replay-rpc`**done.** Read-only `openhuman.agent_run_events` (paged,
`next_offset`, capped limit), `agent_run_status`, `agent_runs_active`
controllers over the C4 journal/status seams; direct `AgentObservation`/
`HarnessRunStatus` serde projection (no PII); registered via
`src/core/all.rs` per the controller-migration checklist. (Resolves the
"replay RPC unexposed" C4 follow-up.)
**Test note:** targeted crate tests (microcompact) ran green locally; the full
core-crate test binary would not link locally under this box's memory ceiling
(single-rustc codegen thrash), so `cargo check --lib --tests` is the local
compile gate and CI runs the actual targeted + full suites + coverage.
## Merge / PR state
- `feat/tinyagents-wave2` pushed to `origin` (senamakel fork); ONE combined PR
vs `upstream` (`--head senamakel:feat/tinyagents-wave2`): **PR #4483**
(`tinyhumansai/openhuman#4483`).
- Submodule branch `feat/microcompact-middleware` pushed to
`tinyhumansai/tinyagents`; gitlink bumped to `ac73382`. Fresh worktrees must
`git submodule update --init vendor/tinyagents`.
- Git etiquette (user rules): push to `origin`, PR against `upstream` with
`--head senamakel:<branch>`; explicit `git add <paths>`; never commit on main.
## Suggested next steps (wave 3)
1. Flip `session_shadow_reads` → reads-from-store once the fixture-matrix
divergence logs are clean (biggest deletion unlock, ~9k incl.
dispatcher/parse/pformat).
2. Flip crate `BudgetMiddleware` → enforcing owner once the 3 documented
criteria hold; delete local `CostBudgetMiddleware` + `turn_subagent_usage.rs`.
3. Open a tinyagents PR for `feat/microcompact-middleware` (currently a pushed
branch, not merged) so the gitlink can later track a tagged release.
4. Continue C5 upstream extractions (multimodal resolver, dialect layer,
overflow-to-artifact, hooks traits) + C2 shadow → authoritative flips.
@@ -1,70 +0,0 @@
# TinyAgents Full Migration Plan
Status: active plan (2026-07-02). Branch: `issue/4249-finish-tinyagents-migration`.
> **2026-07-03 update:** #4249 has landed on main. Remaining work is
> re-planned in [`CONTINUATION-2026-07.md`](CONTINUATION-2026-07.md), which
> supersedes the workstream ordering below and targets tinyagents 1.4/1.5
> (`graph::goals`, `graph::todos`, `NoProgressTracker`, resumable graph
> failures).
>
> **2026-07-04 update:** vendor-crate improvement work (reasoning tokens,
> streaming, performance, native Anthropic provider, upstream extractions)
> is planned separately in
> [`../tinyagents-vendor-improvement-plan/`](../tinyagents-vendor-improvement-plan/)
> (workstreams V1V6, run in parallel with C0C7; this folder's
> `99-deletion-ledger.md` remains the master delete list).
Goal: **hard-migrate** OpenHuman's agent harness onto the `tinyagents` crate as
the library for orchestration, caching, tooling, observability, model
providers, context management, embeddings, sub-agents, steering, summarization,
events, and session storage — then **delete the legacy OpenHuman files**.
OpenHuman keeps only product policy (prompts, registry, security/approval
semantics, credentials, UX projections).
Each workstream folder below is sized for `/goal` execution: the folder
`README.md` states scope + done criteria; numbered step files are individual
goals. Execute a step file end-to-end (code + tests + deletions + commit).
## Key facts superseding older docs
- Current crate is **1.5.0**; the plan still records the earlier "1.3.0 delta"
in `00-baseline.md` where those primitives first became available.
- `docs/tinyagents-sdk-gaps.md` was refreshed against TinyAgents 1.3.0 and
should be re-audited after the 1.5.0 bump; it currently
tracks only residual gaps. tinyagents 1.2.0-1.3.0 ships `UnknownToolPolicy`,
`ToolPolicy` safety metadata + `ToolPolicyMiddleware`, reasoning deltas
(`MessageDelta.reasoning`), durable `JsonlTaskStore` + orchestration tools,
`graph::parallel::map_reduce`, `ModelCatalog` w/ pricing,
`WorkspaceIsolation`, `BudgetMiddleware`, event journals/status stores,
`CapabilityRegistry`, embeddings traits. See `00-baseline.md`.
- Inventory of live/legacy files: `../tinyagents-harness-migration-audit.md`
plus the per-folder deletion lists here (`99-deletion-ledger.md` is the
master list).
## Workstreams (suggested order)
| # | Folder | Theme |
|---|--------|-------|
| 0 | `00-baseline.md` | crate bump, rusqlite 0.40, feature flags |
| 1 | `01-tooling/` | ToolPolicy round-trip, unknown-tool, dynamic exposure, output budgets |
| 2 | `02-models/` | ModelRegistry, profiles/catalog, fallback/retry, reasoning stream |
| 3 | `03-context-cache/` | delete legacy context reducers, ResponseCache + prompt-cache guard |
| 4 | `04-sessions/` | transcript → Store/AppendStore cutover, import phases 24 |
| 5 | `05-events/` | journals/status canonical, bridge consolidation, DomainEvent projection |
| 6 | `06-cost/` | UsageAccounting/Budget middleware, lineage rollup |
| 7 | `07-subagents/` | SubAgent pipeline, detached TaskStore, steering/recursion |
| 8 | `08-orchestration/` | spawn_parallel graph tool, map_reduce, interrupts, graph-stub cleanup |
| 9 | `09-embeddings.md` | EmbeddingModel/VectorStore/Retriever adapters |
| 10 | `10-registry.md` | CapabilityRegistry projection + diagnostics |
| 11 | `11-testing.md` | parity matrix, testkit, conformance (last) |
| 99 | `99-deletion-ledger.md` | master delete list with preconditions |
## Rules (unchanged from spec)
- Never bypass approval/security/sandbox/workspace/credential boundaries.
- JSON-RPC contracts stay stable unless a migration note lands with the change.
- Adapter first, flip ownership on proven parity, **then delete** — deletion is
mandatory, not optional; every step file names its deletions.
- Stage files explicitly (`git add <paths>`), never `git add -A` (see memory).
- Verify `git branch --show-current` before each commit.
-453
View File
@@ -1,453 +0,0 @@
# TinyAgents Harness Migration Audit
Date: 2026-07-01
Scope: current `openhuman-5` checkout on branch `issue/4249-finish-tinyagents-migration`.
TinyAgents refresh: reviewed `tinyhumansai/tinyagents` `main` at
`348a0e7dc71a1f9039f3d523a2a384661a7a9acd` after the current audit was first
written. That repo is now materially ahead of the assumptions in the older
OpenHuman migration docs: it has harness cache/store/session primitives,
sub-agent reuse, graph subgraph/sub-agent nodes, lineage-aware events/status,
and JSONL-backed append stores.
This is a documentation-only audit of how much of the OpenHuman agent harness is
actually migrated to TinyAgents, and which remaining OpenHuman files are good
candidates to port, collapse, or keep as product-specific adapters.
## Bottom Line
The core turn loop is migrated. The live chat turn, channel/CLI turn, and
sub-agent turn all route through `src/openhuman/tinyagents::run_turn_via_tinyagents_shared`.
That means the model/tool iteration loop, TinyAgents middleware stack, event
bridge, stop hooks, context compression, unknown-tool recovery, and tool policy
boundary are on the TinyAgents harness path.
The repository still has a large OpenHuman harness shell around that path:
| Area | Rust files | Lines | Current role |
| ------------------------------------ | ---------: | -----: | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/openhuman/agent/harness/` | 83 | 30,521 | Agent session assembly, transcript compatibility, prompt/tool filtering, context product logic, sub-agent build pipeline, leftover generic loop seams |
| `src/openhuman/tinyagents/` | 13 | 6,481 | TinyAgents adapters, middleware, event bridge, graph helpers, checkpoint adapter |
| `src/openhuman/agent_orchestration/` | 58 | 21,704 | Product orchestration tools, durable workflow/team/session state, graph-backed fanout/delegation wrappers |
The migration should not be read as "OpenHuman can delete `agent/harness/` now."
The correct read is: the execution core is on TinyAgents, but much of the
surrounding runtime was preserved for product compatibility. After reviewing the
current TinyAgents code, more of that surrounding runtime is now portable than
this audit originally assumed: session transcript tracking, prompt/response
cache handling, and sub-agent pipeline orchestration can be expressed through
TinyAgents store/cache/subgraph/sub-agent-node primitives, with OpenHuman keeping
only product policy and compatibility adapters.
## What Is Migrated
### Turn execution chokepoints
- `src/openhuman/agent/harness/session/turn/core.rs` routes the main chat turn
through `super::graph::run_chat_turn_graph`, which is a thin wrapper over
`run_turn_via_tinyagents_shared`.
- `src/openhuman/agent/harness/graph.rs` routes channel/CLI turns through
`run_turn_via_tinyagents_shared`.
- `src/openhuman/agent/harness/subagent_runner/ops/graph.rs` routes sub-agent
turns through `run_turn_via_tinyagents_shared`.
- `src/openhuman/tinyagents/mod.rs` owns the shared harness assembly:
OpenHuman provider adapter, tool adapters, event bridge, middleware, steering,
early-exit hooks, stop hooks, model/tool call caps, and outcome capture.
Practical status: the old in-tree model/tool loop is no longer the live
execution engine for those three entry points.
### Middleware and policy
Already migrated into TinyAgents middleware or the TinyAgents adapter seam:
- Approval and security gating at the tool boundary.
- CLI/RPC-only tool denial.
- Channel permission ceiling and tool policy checks.
- Unknown-tool recovery via an internal sentinel.
- Non-object tool-argument recovery.
- Cost budget pre-checks before model calls.
- Repeated tool failure halting.
- Context compression and message trimming via TinyAgents middleware.
- OpenHuman tool-output budgets and payload summarizer hooks as TinyAgents
tool middleware.
- Stop-hook checks via `src/openhuman/tinyagents/stop_hooks.rs`.
This is real migration, but not complete ownership transfer: OpenHuman still
keeps rich policy metadata outside TinyAgents because the SDK tool schema does
not yet carry all OpenHuman policy fields.
### Graph-backed orchestration
The codebase has several real TinyAgents graph uses:
- `src/openhuman/tinyagents/delegation.rs`: durable plan -> execute -> review ->
finalize graph.
- `src/openhuman/agent_orchestration/workflow_runs/graph.rs`: workflow phase DAG
scheduler graph.
- `src/openhuman/agent_orchestration/agent_teams/graph.rs`: member execution
conditional-routing graph.
- `src/openhuman/tinyagents/orchestration.rs`: reusable `run_parallel_fanout`
helper, used by `spawn_parallel_agents` and workflow phase fanout.
- `src/openhuman/tinyagents/topology.rs`: topology export for fixed graph
structures.
- `src/openhuman/agent_orchestration/running_subagents.rs`: detached sub-agent
lifecycle is mirrored into TinyAgents `InMemoryTaskStore`.
Practical status: graph adoption is meaningful but uneven. The larger product
tools still own validation, persistence, cancellation semantics, compatibility
events, and result formatting.
## What Is Still Mostly OpenHuman-Owned
### Session, transcript, and cache shell
Largest remaining harness surface: `src/openhuman/agent/harness/session/`
at roughly 13.2k lines.
Current OpenHuman role:
- Session transcript migration and compatibility.
- Product-level `AgentBuilder` configuration.
- Prompt section assembly and prompt-cache stability decisions.
- Memory/context injection policy.
- Post-turn hooks, transcript persistence, and OpenHuman-specific history shape.
- Tool dispatcher compatibility for persisted native/XML/P-format transcript
suffixes.
Updated TinyAgents target:
- Use TinyAgents `harness::store::{Store, AppendStore, FileStore,
JsonlAppendStore}` as the durable substrate for message history, event
journals, tool/model call records, artifacts, and local migration outputs.
- Use TinyAgents `harness::cache::{ResponseCache, PromptCacheLayout,
CacheLayoutEvent, CachePolicy}` for response-cache and provider KV-cache
layout protection instead of keeping prompt-cache reasoning as OpenHuman-only
session logic.
- Use TinyAgents run/event/status lineage (`root_run_id`, `parent_run_id`,
offsets, status stores) as the canonical internal transcript/run inspection
surface, then project into legacy OpenHuman views where needed.
- Write a one-time migration script for old OpenHuman `session_raw/*.jsonl` and
legacy Markdown session files into TinyAgents store/journal records. The
script should preserve original session ids, transcript stems, timestamps,
provider/model metadata, tool-call ids, and parent/child links, and should be
idempotent with a marker/version row so it can be safely re-run.
Keep in OpenHuman:
- The legacy reader/writer compatibility layer until migrated sessions are
proven equivalent.
- Product-specific prompt assembly, memory source identity, approval/security
context, and JSON-RPC response shapes.
### Sub-agent build pipeline
Remaining surface: `src/openhuman/agent/harness/subagent_runner/`
at roughly 6.1k lines.
Keep in OpenHuman:
- Agent definition lookup and allowlist enforcement.
- Prompt assembly for archetypes.
- Parent context, memory context, action root, sandbox, and toolkit filtering.
- Worker-thread mirroring and transcript compatibility.
- Integrations-agent preflight and handoff cache behavior.
Updated TinyAgents target:
- Map the OpenHuman sub-agent build pipeline into TinyAgents `SubAgent`,
`SubAgentSession`, and `SubAgentTool` once registry/policy adapters are ready.
- Express multi-step sub-agent work as graph structure using
`graph::subagent_node` for harness-agent leaves and `graph::subgraph` for
reusable pipelines. The current OpenHuman "prepare prompt -> filter tools ->
run child -> checkpoint/handback -> mirror transcript" flow can become a
subgraph rather than a sidecar runner.
- Use TinyAgents parent/root run lineage and `UsageTotals` for child usage/cost
rollup instead of carrying separate OpenHuman aggregation structs.
- Keep OpenHuman-specific preflight nodes for agent definition lookup,
allowlists, toolkit gates, sandbox/action-root narrowing, and worker-thread
mirroring.
### Detached sub-agent registry
Remaining surface: `src/openhuman/agent_orchestration/running_subagents.rs`
at roughly 1.0k lines.
Current state: it uses TinyAgents `InMemoryTaskStore` as a typed lifecycle
ledger, but still owns watch channels, abort handles, wait/steer/cancel control,
tombstones, ownership checks, and session lookup.
Updated TinyAgents target:
- Durable `TaskStore` support.
- Typed wait/steer/cancel APIs.
- Parent/root run tree queries.
- Cancellation requests and hard abort lifecycle events.
TinyAgents now has more of the lifecycle substrate than this audit originally
assumed, including harness/graph status records, lineage-aware events,
`SubAgentSession` reuse, and append stores. The migration should therefore be an
adapter exercise first: map existing durable OpenHuman sub-agent session rows and
worker-thread records into TinyAgents session/status/journal records, then keep
OpenHuman controllers as compatibility projections.
Keep in OpenHuman until parity is proven:
- Desktop restart/resume compatibility.
- Existing JSON-RPC/tool response shapes.
- Durable OpenHuman session and worker-thread ledgers.
### Parallel agents and workflow fanout
Remaining surfaces:
- `src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs`
- `src/openhuman/agent_orchestration/workflow_runs/engine.rs`
- `src/openhuman/tinyagents/orchestration.rs`
Current state: fanout execution uses a TinyAgents graph helper, but
`spawn_parallel_agents` is not yet a first-class graph tool. It still owns
validation, worktree setup, overlap/stale-read checks, compatibility events, and
JSON output formatting.
Good TinyAgents candidates:
- Re-express `spawn_parallel_agents` as `validate -> dispatch -> worker ->
collect -> finalize` using graph `Send` or an SDK map/reduce helper.
- Move deterministic ordering, cancellation boundaries, graph status, and child
lineage into TinyAgents graph state.
- Keep result formatting and OpenHuman worktree policy in a thin wrapper.
### Per-agent graph selectors
There are 32 `src/openhuman/agent_registry/agents/*/graph.rs` files and all
currently return `AgentGraph::Default`.
This is mostly scaffolding, not migrated behavior. It proves the extension seam
exists, but there are no bespoke per-agent TinyAgents graphs yet.
Good candidates for first real custom graphs:
- `orchestrator`: planning/delegation/parallelism policy can become explicit
graph routing instead of prompt-only convention.
- `researcher`: search -> read -> synthesize -> cite can be bounded and
checkpointed.
- `tool_maker`: detect missing capability -> generate -> validate -> expose can
become a graph with explicit review gates.
Candidate cleanup: replace boilerplate default `graph.rs` files with registry
defaults once at least one real custom graph proves the API shape.
## Porting Candidates By Priority
### P0: Update stale active docs and comments
Evidence: active architecture docs still described the old `engine::run_turn_engine`
loop in sections below the TinyAgents status callout.
Action:
- Rewrite active sections to describe TinyAgents as the live loop.
- Move the removed in-house loop details into a short historical appendix.
- Sweep code comments that still say `run_turn_engine`, `run_tool_call_loop`, or
`run_inner_loop` when they now mean the TinyAgents harness path.
Why first: stale docs make it hard to tell whether remaining files are live
runtime, compatibility shell, or historical residue.
Status (2026-07-01): done. `gitbooks/developing/architecture/agent-harness.md`
active sections now describe the TinyAgents path (loop, dialects-as-transcript-
compat, middleware context management, steering-channel cancellation, corrected
file map, 1.2 pin); pre-migration loop details are confined to the marked
historical sections. The retired-loop comment sweep landed earlier (f5a6b5196);
remaining `run_turn_engine`/`run_inner_loop` mentions in code are explicit
"legacy parity" references, not current-behavior claims.
### P1: Migrate old OpenHuman sessions into TinyAgents stores
Design: `docs/tinyagents-session-migration-design.md` (2026-07-01) — source
format inventory, target store layout, lineage-key mapping, idempotency
ledger, fixture matrix, and phasing.
Status (2026-07-01): Phase 1 (write-only importer) implemented in
`src/openhuman/session_import/` as `openhuman.session_import_run`, with the
full fixture matrix as tests. Phases 24 (read-side shadow, cutover,
retirement of legacy readers) remain.
Current OpenHuman files:
- `src/openhuman/agent/harness/session/transcript.rs`
- `src/openhuman/agent/harness/session/migration.rs`
- `session_raw/*.jsonl` and legacy Markdown session directories under user
workspaces.
Target shape:
- Add a one-time migration command/script that reads old OpenHuman session JSONL
and Markdown transcripts, normalizes them into TinyAgents message/event/store
records, and writes them through TinyAgents-compatible `Store`/`AppendStore`
semantics.
- Preserve compatibility metadata so old UI surfaces and run-ledger lookups can
still answer by OpenHuman session key while new internals read by TinyAgents
`thread_id`, `run_id`, `root_run_id`, and stream offset.
- Make the migration idempotent and observable: dry-run mode, per-file summary,
warning list, migrated-count counters, and a marker/version record.
Risk: transcript shape is user data. This needs fixture coverage over current
flat `session_raw/*.jsonl`, older date-folder JSONL, Markdown sessions,
sub-agent transcript stems, native tool-call envelopes, XML/P-format tool
history, and malformed partial files.
### P2: Make event/status journals canonical
Current OpenHuman files:
- `src/openhuman/tinyagents/observability.rs`
- `src/openhuman/agent/harness/engine/progress.rs`
- `src/openhuman/session_db/run_ledger/*`
- `src/core/event_bus/*`
Target shape:
- TinyAgents journals/status stores become the internal source for model/tool
events, usage, graph steps, child run lineage, and resumable inspection.
- `AgentProgress` and `DomainEvent` become compatibility projections.
Risk: this crosses UI streaming, cost footer, run ledgers, and desktop
reconnect behavior. It needs focused parity tests before deletion.
### P3: Port detached task lifecycle beyond the OpenHuman registry
Current OpenHuman files:
- `src/openhuman/agent_orchestration/running_subagents.rs`
- `src/openhuman/agent_orchestration/tools/{wait_subagent,steer_subagent,close_subagent,continue_subagent}.rs`
Target shape:
- TinyAgents owns typed task lifecycle, wait/steer/cancel semantics, parent/root
run lineage, and terminal history.
- OpenHuman owns durable SQL/JSON projection and product response formatting.
This is no longer just waiting on SDK primitives. Current TinyAgents exposes
session reuse, lineage-aware events/status, graph/harness observability, and
JSONL append storage. The concrete blocker is now designing the OpenHuman
compatibility adapter and proving restart/resume parity against existing
controllers.
### P4: Re-express `spawn_parallel_agents` as a graph tool
Current OpenHuman files:
- `src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs`
- `src/openhuman/agent_orchestration/worktree.rs`
- `src/openhuman/tinyagents/orchestration.rs`
Target shape:
- A graph with nodes for validation, dispatch, worker, collect, finalize.
- Deterministic reducer state for per-task result order.
- Child run lineage and cancellation through graph status.
- Thin OpenHuman wrapper for the existing JSON result and worktree policy.
Why: this is a high-value migration because it converts a large product-visible
orchestration loop without touching the basic chat turn.
### P5: Replace generic checkpoint/progress seams
Current OpenHuman files:
- `src/openhuman/agent/harness/engine/checkpoint.rs`
- `src/openhuman/agent/harness/engine/progress.rs`
- `src/openhuman/agent/harness/subagent_runner/ops/checkpoint.rs`
Target shape:
- Model-call cap, early-exit pause, resumable checkpoint summary, and progress
projection represented as TinyAgents status/events/middleware.
Keep only the OpenHuman-specific compatibility formatting for existing
transcripts and tool outputs.
### P6: Retire boilerplate per-agent graph selectors
Current OpenHuman files:
- `src/openhuman/agent/harness/agent_graph.rs`
- `src/openhuman/agent_registry/agents/*/graph.rs`
Target shape:
- Default graph supplied centrally.
- Only agents with custom graph behavior keep a `graph.rs`.
- Registry diagnostics show which graph each agent actually uses.
This should happen after at least one bespoke graph lands, so the cleanup does
not remove a seam before it has proved useful.
## Code That Should Probably Stay In OpenHuman
Do not port these blindly into TinyAgents:
- Agent registry definitions and built-in prompt semantics.
- Tool allowlists/denylists that encode product policy.
- Security policy, sandbox roots, approval records, credential access, and
workspace/action-dir boundaries.
- Session transcript migration and persisted compatibility formats.
- OpenHuman model provider routing, billing classification, and credential
ownership.
- UI-facing JSON-RPC response shapes and `DomainEvent` compatibility until every
subscriber is moved.
- Worktree isolation policy and dirty-worktree safeguards.
- Memory stores, retrieval policy, and context source identity.
TinyAgents should own generic runtime machinery. OpenHuman should own product
semantics and compatibility boundaries.
## Main SDK Gaps Blocking More Deletion
The most important upstream gaps are tracked in `docs/tinyagents-sdk-gaps.md`,
but that file must be refreshed against TinyAgents `main` at
`348a0e7dc71a1f9039f3d523a2a384661a7a9acd`. Several older "missing" items now
exist as concrete TinyAgents modules:
- `harness::cache` has response caching and prompt/KV-cache layout protection.
- `harness::store` has key-value stores and `JsonlAppendStore`.
- `harness::subagent` has `SubAgent`, `SubAgentSession`, and `SubAgentTool`.
- `graph::subgraph` and `graph::subagent_node` can express nested graph and
sub-agent pipelines.
- harness/graph observability and status types carry `root_run_id` /
`parent_run_id` lineage.
Remaining blockers are narrower:
- Rich tool policy metadata in TinyAgents schemas.
- Recoverable unknown-tool policy without the OpenHuman sentinel.
- Full OpenHuman policy mapping for approval/security/sandbox/workspace roots.
- A migration adapter from OpenHuman session JSONL/Markdown/run-ledger rows into
TinyAgents store/journal/status records.
- SQLite/native-link compatibility if OpenHuman wants to use TinyAgents'
embedded SQLite backend directly rather than its own SQLite adapter.
- Redaction/cursor/backfill rules for production UI replay.
- OpenHuman controller compatibility during the migration window.
## Suggested Next Documentation Pass
1. Rewrite the active `gitbooks/developing/architecture/agent-harness.md`
sections below the status block so they describe the TinyAgents path instead
of the removed in-house loop.
2. Add a one-time transcript/session migration design for old OpenHuman
`session_raw/*.jsonl` and Markdown sessions into TinyAgents store/journal
records. Done: `docs/tinyagents-session-migration-design.md`.
3. Add a small "Historical pre-TinyAgents loop" appendix for details that are
still useful context.
4. Add per-folder READMEs or module docs for:
- `src/openhuman/agent/harness/session/`: product session shell over
TinyAgents.
- `src/openhuman/agent/harness/subagent_runner/`: OpenHuman sub-agent build
pipeline over TinyAgents.
- `src/openhuman/agent_orchestration/`: product orchestration wrappers over
TinyAgents graphs/task lifecycle.
5. Keep `docs/tinyagents-migration-spec.md` as the backlog, and use this audit
as the current inventory snapshot.
-836
View File
@@ -1,836 +0,0 @@
# TinyAgents Migration Spec
Status: draft migration backlog
TinyAgents source reviewed: `tinyhumansai/tinyagents` `origin/main` at
`8f226f1`, crate version `1.1.0`. Refreshed against `tinyhumansai/tinyagents`
`main` at `348a0e7dc71a1f9039f3d523a2a384661a7a9acd` after the SDK/docs update.
Current OpenHuman dependency in this checkout is
`tinyagents = { version = "1.5.0", features = ["sqlite"] }`.
OpenHuman already depends on TinyAgents and already routes the live agent turn
through `src/openhuman/tinyagents/`. This spec is not a proposal to add
TinyAgents. It is a todo list for moving the rest of OpenHuman's generic agent
runtime behavior onto TinyAgents primitives while keeping OpenHuman-owned
product semantics in OpenHuman.
Current inventory snapshot: [`tinyagents-harness-migration-audit.md`](tinyagents-harness-migration-audit.md).
## Goal
Use TinyAgents as the generic runtime for:
- model/provider abstraction and model selection
- tools and tool schemas
- middleware around model/tool calls
- streaming, events, traces, and replayable run status
- session transcript storage and migration targets
- prompt/response cache layout protection
- token usage and cost rollups
- state graphs, fanout, reducers, checkpoints, and interrupts
- sub-agent recursion, steering, cancellation, and reusable sessions
- deterministic testkit coverage for the runtime seams
OpenHuman should continue to own:
- desktop product UX and Tauri/RPC boundaries
- user/workspace config, credentials, keychain, and approval records
- OpenHuman memory stores, thread transcripts, run ledgers, and controllers
- security policy, sandboxing, tool permission tiers, and workspace roots
- product-specific built-in agents, prompts, MCP setup, Composio, channels, and
native tools
- compatibility with existing JSON-RPC method names and persisted state
## Sources Reviewed
TinyAgents SDK:
- `src/lib.rs`
- `src/harness/*`
- `src/graph/*`
- `src/registry/*`
- `src/language/*`
- `src/repl/*`
- `docs/modules/harness/*.md`
- `docs/modules/graph/*.md`
- `docs/modules/registry/*.md`
- `docs/modules/expressive-language/README.md`
- `docs/modules/repl-language/README.md`
- examples: `agent_loop_tools`, `orchestrator_subagents`, `durable_graph`,
`openai_graph_agent`, `openai_self_blueprint`
OpenHuman Rust core:
- `src/openhuman/tinyagents/*`
- `src/openhuman/agent/**`
- `src/openhuman/agent_orchestration/**`
- `src/openhuman/agent_registry/**`
- `src/openhuman/tools/**`
- `src/openhuman/inference/**`
- `src/openhuman/cost/**`
- `src/openhuman/context/**`
- `src/openhuman/approval/**`
- `src/openhuman/security/**`
- `src/openhuman/mcp_registry/**`
- `src/core/event_bus/**`
- `src/core/all.rs`
- `gitbooks/developing/architecture/agent-harness.md`
## Current Adoption Inventory
Already done or partially done:
- `Cargo.toml` pins `tinyagents = { version = "1.5.0", features = ["sqlite"] }`.
- `src/openhuman/tinyagents/mod.rs` registers OpenHuman `Provider` and `Tool`
adapters on `tinyagents::harness::runtime::AgentHarness`.
- `ProviderModel` maps OpenHuman `ChatRequest`/`ChatResponse` into
`tinyagents::harness::model::{ModelRequest, ModelResponse, ModelStream}`.
- `ToolAdapter` and `SharedToolAdapter` map OpenHuman tools into
`tinyagents::harness::tool::Tool`.
- `OpenhumanEventBridge` maps TinyAgents `AgentEvent` into `AgentProgress` and
the global cost tracker.
- `StopHookMiddleware`, `ContextCompressionMiddleware`, and
`MessageTrimMiddleware` are already used on the TinyAgents path.
- `SqlRunLedgerCheckpointer` implements TinyAgents `Checkpointer` on top of the
OpenHuman session DB while the migration re-points checkpoint rows to the
crate checkpointer.
- `spawn_parallel_graph` uses `GraphBuilder` and
`graph::parallel::map_reduce` for reusable concurrent fanout.
- `model_council`, `workflow_runs`, `agent_teams`, and
`tinyagents/delegation.rs` already use TinyAgents graphs.
- Built-in agents already have `graph.rs` selectors, but most return
`AgentGraph::Default`.
Important current gaps:
- OpenHuman still has separate registries for agents, tools, MCP tools, model
providers, controllers, cost, and event bus projections.
- Tool safety metadata exists in OpenHuman traits but is not fully expressed as
TinyAgents tool safety/runtime metadata.
- Cost/usage is still converted through a bridge, not an end-to-end TinyAgents
usage/cost journal.
- Event streams are mirrored into `AgentProgress`, but TinyAgents event journals
and status stores are not the canonical durable inspection surface.
- Provider model profiles, model resolution, and fallback remain primarily in
OpenHuman inference/router logic.
- Sub-agent lifecycle, durable state, worker threads, and wait/abort controls
still live in OpenHuman orchestration stores.
Some todos below are local adapter work. Others still require upstream
TinyAgents SDK extensions. In particular, the SDK has a strong tool/runtime
boundary today (`ToolSchema`, `ToolExecutionContext`, middleware hooks), but
OpenHuman's full tool safety metadata is richer than the current SDK schema.
After the TinyAgents `main` refresh, durable session/cache/journal primitives are
no longer the broad SDK gap they were in the original baseline; the remaining
work is to design OpenHuman's compatibility adapter, migrate old transcripts and
run-ledger rows, and prove restart/resume parity.
## Migration Rules
- Keep every product-facing JSON-RPC contract stable unless a migration plan is
written next to the code change.
- Do not bypass OpenHuman approval, security policy, sandbox, workspace root, or
credential boundaries by adopting a generic TinyAgents tool API.
- Prefer adapters first, then flip ownership once tests prove parity.
- Preserve existing transcript and run-ledger compatibility. TinyAgents may
become the internal runtime without changing persisted public records in the
same PR.
- Old OpenHuman session JSONL/Markdown data should move through a one-time,
idempotent migration script into TinyAgents-compatible store/journal records
before the old readers are deleted.
- Every migration task needs unit coverage plus at least one JSON-RPC or
harness-level e2e when behavior crosses controller, tool, provider, or graph
boundaries.
## Phase 0 - Baseline And Drift Control
- [x] Add a version/feature compatibility note to the OpenHuman architecture doc.
- OpenHuman files: `gitbooks/developing/architecture/agent-harness.md`,
`Cargo.toml`.
- TinyAgents components: crate features `default`, `openai`, `sqlite`, `repl`.
- Acceptance: document why default features are used, why TinyAgents `sqlite`
is enabled through the aligned `rusqlite` stack, and which OpenHuman adapters
still replace SDK-owned providers.
- **Done:** added "TinyAgents crate: features & compatibility" section to
agent-harness.md (default-only, `openai`/`sqlite`/`repl` rationale, adapter
map) + fixed stale `council_graph.rs`/`member_graph.rs` links.
## Phase 1 - Tools
- [~] Make OpenHuman tool metadata round-trip into TinyAgents tool metadata.
- OpenHuman files: `src/openhuman/tools/traits.rs`,
`src/openhuman/tinyagents/tools.rs`, `src/openhuman/tinyagents/convert.rs`.
- TinyAgents components: `harness::tool::{ToolSchema, ToolFormat,
ToolExecutionContext, ToolResult}`.
- Migrate: permission level, external effect, generated runtime context,
timeout policy, concurrency safety, result-size cap, display label/detail,
markdown support.
- Acceptance: a TinyAgents tool call has enough metadata for middleware to
enforce approval, security, timeout, concurrency, truncation, and display
behavior without re-querying OpenHuman trait methods ad hoc.
- **1.3 update:** crate `ToolPolicy`, `Tool::policy()`,
`ToolRegistry::policies()`, and `ToolPolicyMiddleware` now provide the
SDK-owned safety/runtime/access projection. `ToolSchema` still carries only
name/description/parameters/format — it has **no** model-visible
metadata/extension map — so OpenHuman should map enforcement fields into
`ToolPolicy` while keeping display/schema annotations as app-side metadata.
Existing side-lookup middleware (`ApprovalSecurityMiddleware`,
`ToolOutputMiddleware`) can shrink as those policy snapshots become the
source of truth.
- [x] Move unknown-tool recovery into a reusable middleware or tool policy layer.
- Current path: `run_policy_for` sets
`UnknownToolPolicy::ReturnToolError`; no sentinel tool is registered.
- TinyAgents components: `ToolRegistry`, `ToolMiddleware`,
`AgentEvent::ToolStarted/ToolCompleted`, repairable tool results.
- Acceptance: hallucinated tool names remain recoverable, sub-agent wording is
preserved, and TinyAgents event stream records the original requested tool
name without exposing the sentinel as a model-visible tool.
- **1.3 update:** crate `RunPolicy.unknown_tool` now has
`UnknownToolPolicy::{Fail, ReturnToolError, Rewrite}` and emits
`AgentEvent::UnknownToolCall` with the original requested name/arguments.
OpenHuman now uses that policy directly; `UNKNOWN_TOOL_SENTINEL` and
`UnknownToolRewriteMiddleware` are gone from source.
- [x] Route approval and security through TinyAgents middleware.
- Current OpenHuman files: `src/openhuman/approval/*`,
`src/openhuman/security/*`, `src/openhuman/tinyagents/tools.rs`.
- TinyAgents components: `ToolMiddleware`, `ToolExecutionContext`,
tool safety metadata.
- Acceptance: approval checks happen in `before_tool`/`wrap_tool`, emit typed
events, preserve audit rows, and return model-consumable denial results.
- **Done:** `ApprovalSecurityMiddleware` (`tinyagents/middleware.rs`, a
`wrap_tool` middleware) replaces the inline approval block in
`execute_openhuman_tool`. Denials short-circuit with a model-consumable
result; approved external-effect calls now record a terminal audit row
(`record_execution`) the old path dropped. Typed approval events still ride
`DomainEvent` (the crate `AgentEvent` enum has no approval variant — SDK gap).
Tool-*internal* security (path/command `live_policy`) stays per-tool by
design. Follow-ups: channel permission-ceiling threading; per-tool metadata
side-lookup (Task C).
- [ ] Use TinyAgents bounded-concurrent tool execution where safe.
- OpenHuman files: `src/openhuman/tools/traits.rs`,
`src/openhuman/tinyagents/tools.rs`.
- TinyAgents components: graph `Send`, graph fanout, or harness tool
execution policy.
- Acceptance: read-only/concurrency-safe tool batches can run in parallel
with deterministic result ordering and identical transcript semantics.
## Phase 2 - Models And Providers
- [ ] Register OpenHuman inference providers as TinyAgents model registry entries.
- OpenHuman files: `src/openhuman/inference/provider/*`,
`src/openhuman/inference/model_ids.rs`, `src/openhuman/tinyagents/model.rs`.
- TinyAgents components: `harness::model::{ChatModel, ModelRegistry,
ModelProfile, CapabilitySet, ModelRequest, ModelResponse}`.
- Acceptance: every workload route (`agentic`, `reasoning`, `coding`,
`memory`, `subconscious`, etc.) can resolve to a TinyAgents model entry
while retaining OpenHuman provider strings and config compatibility.
- [~] Translate OpenHuman provider capability data into TinyAgents model profiles.
- OpenHuman files: `src/openhuman/inference/provider/traits.rs`,
`src/openhuman/inference/provider/factory.rs`,
`docs/inference-provider-catalog.md`.
- TinyAgents components: `ModelProfile`, `CapabilitySet`, registry model
catalog.
- Acceptance: context window, tool calling, streaming, vision, structured
output, reasoning, local/cloud source, and provider-family metadata are
available before dispatch.
- **Partial:** every `ProviderModel` registered by the shared runner now
carries a crate `ModelProfile` built at construction from the provider's
canonical capability accessors — tool calling (+parallel), vision
(`modalities.image_in`), streaming, local/remote source — plus the
runner-threaded token limits (`with_context_window` → `max_input_tokens`,
output cap → `max_output_tokens`). `ChatModel::profile()` returns it, so
the crate's pre-dispatch validation and structured-output strategy see real
capabilities. Remaining: structured-output/JSON-schema/reasoning flags
(no OpenHuman capability source yet), release/status metadata, and a
registry-level model *catalog* (ties into the workload-route registry item
above).
- [ ] Move model fallback and retry policy to TinyAgents policy/middleware.
- OpenHuman files: `src/openhuman/inference/provider/reliable.rs`,
`src/openhuman/inference/provider/router.rs`,
`src/openhuman/tinyagents/mod.rs`.
- TinyAgents components: `RunPolicy`, `RetryPolicy`, `FallbackPolicy`,
`ModelFallbackMiddleware`, `AgentEvent::RetryScheduled`,
`AgentEvent::FallbackSelected`.
- Acceptance: OpenHuman provider retry does not double-retry under
TinyAgents, fallback events are typed, and tests cover transient 429/5xx,
config rejection, and billing exhaustion.
- [ ] Preserve provider-specific metadata in the TinyAgents message model.
- OpenHuman files: `src/openhuman/inference/provider/traits.rs`,
`src/openhuman/tinyagents/convert.rs`,
`src/openhuman/tinyagents/model.rs`.
- TinyAgents components: `ContentBlock`, `AssistantMessage`, provider
metadata, tool-call ids.
- Acceptance: Gemini thought signatures, reasoning content, native tool-call
ids, cached tokens, and raw provider metadata survive multi-turn history.
## Phase 3 - Middleware
- [ ] Convert OpenHuman turn cross-cuts into named TinyAgents middleware.
- Current OpenHuman surfaces: stop hooks, approval gate, security policy,
output caps, context compression, memory injection, tool allowlists,
cost/usage, prompt cache stability, event bridge.
- TinyAgents components: `Middleware`, `ModelMiddleware`, `ToolMiddleware`,
`MiddlewareStack`, `RunContext`.
- Acceptance: each cross-cut has a stable middleware name, tests for ordering,
emitted events, and explicit interaction with streaming/retry/fallback.
- [ ] Add OpenHuman policy middleware for dynamic tool exposure.
- OpenHuman files: `src/openhuman/agent_registry/*`,
`src/openhuman/agent/harness/subagent_runner/**`,
`src/openhuman/tools/user_filter.rs`.
- TinyAgents components: `before_model`, `before_tool`, `ToolRegistry`.
- Acceptance: agent `tool_allowlist`, `tool_denylist`, sub-agent tool scope,
MCP tool visibility, and channel permission ceilings are enforced through
middleware rather than scattered pre-filtering.
- [ ] Add prompt/cache-layout middleware tests.
- OpenHuman files: `src/openhuman/context/*`,
`src/openhuman/agent/harness/session/turn/core.rs`,
`src/openhuman/tinyagents/summarize.rs`.
- TinyAgents components: `harness::cache::{CachePolicy, CacheLayoutEvent,
PromptCacheLayout, ResponseCache}`, `ContextCompressionMiddleware`,
`MessageTrimMiddleware`.
- Acceptance: system prompt prefix remains stable across later turns; volatile
memory, timestamps, tool results, and steering messages land in the tail.
- [ ] Move OpenHuman response-cache and provider KV-cache protection onto
TinyAgents cache primitives.
- OpenHuman files: `src/openhuman/agent/harness/session/turn/core.rs`,
`src/openhuman/context/*`, `src/openhuman/tinyagents/middleware.rs`.
- TinyAgents components: `harness::cache::{ResponseCache,
PromptCacheLayout, CachePolicy}`, `AgentEvent::CacheHit`,
`AgentEvent::CacheMiss`.
- Acceptance: repeated deterministic model requests can be served by the
TinyAgents response cache where safe, prompt-prefix stability is asserted by
`PromptCacheLayout`, and OpenHuman cache-align warnings become TinyAgents
cache-layout events.
## Phase 4 - Events, Status, And Observability
- [ ] Make TinyAgents event journals the canonical internal run event stream.
- OpenHuman files: `src/openhuman/tinyagents/observability.rs`,
`src/core/event_bus/*`, `src/openhuman/notifications/*`,
`src/openhuman/session_db/run_ledger/*`.
- TinyAgents components: `HarnessEventJournal`, `HarnessStatusStore`,
`GraphEventJournal`, `GraphStatusStore`, `harness::store::AppendStore`,
`harness::store::JsonlAppendStore`, `AgentEvent`, `GraphEvent`.
- Acceptance: UIs can reconstruct a running or completed agent turn from
persisted TinyAgents events without relying only on transient
`AgentProgress`.
- [x] Write a one-time OpenHuman session transcript migration into TinyAgents
store/journal records. Done: `src/openhuman/session_import/`
(`openhuman.session_import_run`), design + as-built notes in
`docs/tinyagents-session-migration-design.md`. Write-only Phase 1; read-side
shadow/cutover remain.
- OpenHuman files: `src/openhuman/agent/harness/session/transcript.rs`,
`src/openhuman/agent/harness/session/migration.rs`, user workspace
`session_raw/*.jsonl`, legacy Markdown session directories.
- TinyAgents components: `harness::store::{Store, AppendStore, FileStore,
JsonlAppendStore}`, `harness::message::Message`, harness event/status
records with `thread_id`, `run_id`, `root_run_id`, and stream offsets.
- Acceptance: old OpenHuman sessions are imported idempotently, preserving
timestamps, transcript stems, parent/child session links, provider/model
metadata, tool-call ids, native/XML/P-format history, and malformed-file
warnings. Existing OpenHuman readers remain as compatibility projections
until parity fixtures prove the migration.
- [ ] Bridge TinyAgents events into `DomainEvent` as a compatibility projection.
- OpenHuman files: `src/core/event_bus/events.rs`,
`src/openhuman/agent/bus.rs`, `src/openhuman/tinyagents/observability.rs`.
- Acceptance: existing subscribers continue to receive `DomainEvent`, but new
code reads TinyAgents events/status first.
- [ ] Persist graph run status and checkpoint metadata in OpenHuman run ledger.
- OpenHuman files: `src/openhuman/tinyagents/checkpoint.rs`,
`src/openhuman/session_db/run_ledger/store.rs`,
`src/openhuman/agent_orchestration/**`.
- TinyAgents components: `Checkpoint`, `CheckpointMetadata`,
`GraphRunStatus`, `GraphObservation`.
- Acceptance: command center, workflow runs, delegation, and team runs can
list checkpoints, current node/task status, and replay offsets from one DB
source.
- [x] Export graph topology for debugging and UI inspection.
- OpenHuman files: built-in `graph.rs` files, `agent_orchestration/*/graph.rs`,
`model_council/graph.rs`.
- TinyAgents components: `GraphTopology`, `to_json`, `to_mermaid`,
validation report.
- Acceptance: every custom OpenHuman graph has a debug endpoint or test
snapshot that exports topology and validates missing nodes/routes.
- **Done:** `tinyagents/topology.rs` — `GraphTopologyReport` (mermaid + JSON +
validation errors/warnings), `describe()`, and `all_graph_topologies()`.
Pattern: each graph exposes a `build_*_graph` (structure) reused by both the
runner and a `*_topology()` that builds it with no-op stub closures and
returns `CompiledGraph::topology()`. Exported graphs: `agent_teams:member`,
`delegation` (extracted `build_delegation_graph`),
`workflow_runs:scheduler` (`build_scheduler_graph` with injected
`select`/`run` engine effects), `subagent:pipeline`, and
`spawn_parallel_agents`. Generic map-reduce fan-outs such as council runs
are still item-count-driven dispatch→N→collect patterns, not fixed named
topologies, so they are intentionally not exported. Debug endpoint:
`agent.graph_topologies` JSON-RPC controller (`agent/schemas.rs`) returning
`{name, ok, errors, warnings, mermaid, topology}` per graph.
## Phase 5 - Usage, Cost, And Budgets
- [~] Replace bridge-only usage accounting with TinyAgents usage records.
- OpenHuman files: `src/openhuman/cost/*`,
`src/openhuman/tinyagents/observability.rs`,
`src/openhuman/inference/provider/traits.rs`.
- TinyAgents components: `harness::usage::{Usage, UsageTotals}`,
`harness::cost::CostTotals`, `AgentEvent::UsageRecorded`.
- Acceptance: input, output, cached input, reasoning, image/audio, embedding,
tool/model call counts, and estimated/provider-reported source are recorded
in normalized records.
- **Partial (real bug fixed):** the bridge hardcoded `charged_amount_usd: 0.0`
and `ProviderModel` dropped cached tokens, so EVERY tinyagents turn recorded
**$0 cost**. Now `model.rs` carries `cached_input_tokens` via crate
`Usage.cache_read_tokens`, and the bridge estimates per-call cost from
catalogued per-MTok rates (`cost::catalog::estimate_cost_usd`). Remaining:
reasoning/image/audio/embedding token fields, model/tool call counts, and an
explicit estimated-vs-provider-charged `cost_source` tag on `TokenUsage`
(provider-charged preservation needs an out-of-band carry — crate `Usage` has
no USD field).
- [~] Move budget checks to pre-call TinyAgents middleware.
- OpenHuman files: `src/openhuman/cost/tracker.rs`,
`src/openhuman/tinyagents/mod.rs`.
- TinyAgents components: `RunPolicy`, cost middleware, `before_model`,
`before_tool`.
- Acceptance: per-run, per-thread, daily, and monthly budgets can warn or
fail before spend where enough data exists, then reconcile after provider
usage is known.
- **Partial:** `CostBudgetMiddleware` (`before_model`) fails the run before a
model call when the global daily/monthly budget is already exceeded
(`CostTracker::check_budget`), and logs on the warning threshold. Self-gating
on `config.enabled`; previously daily/monthly enforcement was dormant on the
tinyagents path. Remaining: per-run/per-thread budgets (need new
`CostConfig` fields + thread-id threading into the runner) and projecting the
*next* call's cost pre-spend (needs an input-token estimate).
- [~] Add cost rollup across sub-agents and graphs.
- OpenHuman files: `src/openhuman/agent_orchestration/**`,
`src/openhuman/cost/global.rs`.
- TinyAgents components: run ids, parent/root run lineage,
`SubAgentStarted`, `SubAgentCompleted`, graph child runs.
- Acceptance: parent run totals include child agent/model/tool usage without
double counting dashboard totals.
- **Partial (audit + real gap fixed).** Audit of the current mechanics:
(1) parent-turn rollup — the `turn_subagent_usage` task-local collector
wraps the turn future; `run_typed_mode` (the single sub-agent chokepoint)
records every inline child, and graph fan-outs (`spawn_parallel_graph`,
delegation, council) execute via `join_all` **on the same task**, so their
children inherit the collector too; (2) dashboard totals — the global
tracker is fed per model call by each run's own event bridge, and the
parent's fold into `LastTurnUsage`/transcript never re-records to the
tracker, so there is no double counting. **Gap fixed:** the tracker feed
lived *only* in the bridge, so an unobserved (`on_progress = None`,
fire-and-forget) turn's spend never reached the dashboard — the runner's
cost fallback now records the aggregate via `record_unobserved_turn_usage`
(mutually exclusive with the bridge → exactly-once). Remaining: crate
run-id / parent-root lineage on cost records (needs `TokenUsage` schema
fields), and rollup for *detached* background children beyond
global-tracker capture (documented behavior today).
## Phase 6 - Graph Runtime And Orchestration
- [ ] Convert remaining ad hoc control loops into explicit TinyAgents graphs.
- Candidate OpenHuman files: `src/openhuman/agent_orchestration/*`,
`src/openhuman/subconscious/*`, `src/openhuman/cron/*`,
`src/openhuman/learning/*`, `src/openhuman/tools/ops.rs`.
- TinyAgents components: `GraphBuilder`, `Command`, conditional routing,
reducers, `Send`, barriers, recursion policy.
- Acceptance: every long-running multi-step orchestration has named nodes,
route tests, recursion bounds, cancellation checks, and topology export.
- [ ] Replace simple fanout helpers with graph `Send` where payload-specific
fanout matters.
- Current helper: `src/openhuman/tinyagents/orchestration.rs`.
- TinyAgents components: `Command::send`, `GraphInput`, `NodeContext::send_arg`.
- Acceptance: map-reduce style flows can schedule multiple invocations of the
same node with distinct payloads instead of materializing one node per item.
- [ ] Make `spawn_parallel_agents` a first-class TinyAgents graph tool.
- Current OpenHuman files:
`src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs`,
`src/openhuman/tinyagents/orchestration.rs`,
`src/openhuman/agent_orchestration/worktree.rs`,
`src/openhuman/agent_orchestration/workflow_runs/engine.rs`.
- Current behavior: validates at least two tasks, checks parent context,
enforces `max_parallel_tools`, resolves `AgentDefinition`s, enforces the
parent `subagents.allowlist`, optionally creates per-worker git worktrees,
fans workers out through `spawn_parallel_graph` + `map_reduce`, collects results in input
order, emits `DomainEvent` + `AgentProgress`, detects stale parent file
reads and cross-worker changed-file overlaps, and returns a structured
`parallel_agents` JSON payload.
- TinyAgents components: `GraphBuilder`, `Command`, `Send`,
`NodeContext::send_arg`, `ChannelSet`/reducers, `GraphEventSink`,
`SubAgentNode` or OpenHuman `run_subagent` adapter, `RecursionPolicy`,
`RunPolicy`, `CancellationToken`.
- Required migration shape:
- `validate` node: parse tasks, enforce min/max count, parent context,
allowlist, toolkit requirements, and worktree preflight.
- `dispatch` node: use `Send` to schedule one worker invocation per task
instead of generating one static worker node per task.
- `worker` node: run the OpenHuman sub-agent build pipeline with inherited
model/tool/security policy, optional `worktree_action_dir`, child task id,
and bounded turn/output budgets.
- `collect` reducer: aggregate successes, failures, elapsed time,
iterations, worktree status, changed files, and stale read markers in
deterministic task order.
- `finalize` node: emit compatibility `DomainEvent`/`AgentProgress`
projections, overlap warnings, and the existing JSON result shape.
- Acceptance: parallel agent runs are checkpointable, cancellable at graph
boundaries, bounded by parent policy, observable through TinyAgents graph
events/status, compatible with existing `spawn_parallel_agents` tool
output, and able to run edit-capable workers in isolated worktrees without
silently falling back to shared workspace.
- [ ] Define parallel-agent ownership and scheduling policy explicitly.
- OpenHuman files: `src/openhuman/agent_registry/agents/orchestrator/prompt.md`,
`src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs`,
`src/openhuman/tools/traits.rs`.
- TinyAgents components: graph route metadata, `RunPolicy`, task metadata,
tool safety metadata.
- Required policy:
- The parent must provide disjoint ownership boundaries for write-capable
tasks, or the graph rejects/falls back to serial delegation.
- Read-only workers may share the parent workspace; write-capable workers
should request `isolation = "worktree"`.
- Parent and children inherit one root run id for cost/event rollups, but
each child gets its own task id and optional worker-thread id.
- A child cannot widen tools, model choice, sandbox mode, trusted roots, or
budget beyond the parent-granted policy.
- Cancellation, steering, and wait/collect must be delivered at graph or
harness safe boundaries only.
- Acceptance: the orchestrator can ask for parallelism without prompt-only
conventions; policy violations become structured graph/tool errors.
- [ ] Make per-agent `graph.rs` selectors real customization points.
- OpenHuman files: `src/openhuman/agent_registry/agents/*/graph.rs`,
`src/openhuman/agent/harness/agent_graph.rs`.
- TinyAgents components: `CompiledGraph`, sub-agent nodes, graph testkit.
- Acceptance: at least three agents get bespoke graphs where useful:
orchestrator, researcher, and tool_maker are good first candidates.
- [~] Keep durable orchestration stores OpenHuman-owned until the OpenHuman
compatibility adapter is written.
- Current OpenHuman files: `running_subagents.rs`, `workflow_runs`,
`agent_teams`, `command_center`, `subagent_sessions`.
- Updated TinyAgents status: current `main` has harness stores, JSONL append
journals, lineage-aware harness/graph status, `SubAgentSession`, subgraph
nodes, and sub-agent graph nodes. This means the blocker is no longer only
"SDK lacks storage"; it is now the OpenHuman adapter/migration design and
restart/resume parity.
- Acceptance: migrate durable SQL/JSON state through a compatibility adapter,
not by dropping records into in-memory task storage. OpenHuman controllers
continue to read compatible projections while TinyAgents records become the
canonical internal state.
- [ ] Add graph interrupt/resume for human review points.
- OpenHuman files: `src/openhuman/approval/*`,
`src/openhuman/agent_orchestration/workflow_runs/*`,
`src/openhuman/tinyagents/delegation.rs`.
- TinyAgents components: `Interrupt`, `ResumeTarget`, `Command::resume`,
checkpoints.
- Acceptance: approval/review pauses are durable graph interrupts where the
run can resume from the exact checkpoint after user action.
## Phase 7 - Sub-Agents, Steering, And Recursion
- [ ] Represent `spawn_subagent`, `steer_subagent`, `wait_subagent`, and
follow-ups as TinyAgents steering commands plus OpenHuman projections.
- OpenHuman files: `src/openhuman/agent_orchestration/tools.rs`,
`src/openhuman/agent_orchestration/running_subagents.rs`,
`src/openhuman/agent/harness/subagent_runner/**`.
- TinyAgents components: `SteeringCommand`, `SteeringHandle`,
`SubAgentSession`, `SubAgentTool`, recursion depth events.
- Acceptance: mid-flight messages are delivered only at safe loop boundaries,
accepted/rejected steering emits events, and tool/model allowlists can only
narrow from parent policy.
- [ ] Re-express the OpenHuman sub-agent pipeline as a TinyAgents subgraph.
- OpenHuman files: `src/openhuman/agent/harness/subagent_runner/**`,
`src/openhuman/agent_orchestration/tools/spawn_subagent.rs`,
`src/openhuman/agent_orchestration/subagent_sessions/**`.
- TinyAgents components: `harness::subagent::{SubAgent, SubAgentSession,
SubAgentTool}`, `graph::subagent_node::{SubAgentNode, SubAgentPolicy}`,
`graph::subgraph::{shared_subgraph_node, adapter_subgraph_node}`,
`CapabilityRegistry`, graph status/observability.
- Acceptance: definition resolution, tool filtering, prompt assembly,
toolkit preflight, sandbox/action-root narrowing, handoff cache,
checkpoint/awaiting-user handback, and worker-thread mirroring are explicit
graph nodes or adapters. The final child run has TinyAgents lineage, status,
usage/cost rollup, and transcript storage; OpenHuman keeps only product
policy nodes and compatibility response formatting.
- [ ] Reconcile OpenHuman spawn depth with TinyAgents recursion policy.
- OpenHuman files: `src/openhuman/agent/harness/spawn_depth_context.rs`,
`src/openhuman/agent/harness/subagent_runner/**`.
- TinyAgents components: `RecursionPolicy`, `RecursionStack`,
`SubAgentDepth`.
- Acceptance: there is one authoritative recursion cap and one error shape,
with compatibility conversion for existing UI/JSON-RPC responses.
- [~] Keep OpenHuman's sub-agent build pipeline as product policy, but move the
pipeline mechanics into TinyAgents graph/sub-agent primitives.
- Product-owned pieces: agent definition resolution, prompt assembly, memory
context, worker-thread mirroring, handoff cache, tool filtering, provider
routing, sandbox scope.
- TinyAgents components to adopt beneath it: `SubAgentSession`,
`SubAgentTool`, `SubAgentNode`, `SubAgentPolicy`, `ToolExecutionContext`,
event lineage, graph status, cancellation, usage/cost rollup.
- Acceptance: use TinyAgents for execution, transcript/session tracking,
graph structure, and lineage without flattening OpenHuman's agent registry
semantics into generic SDK defaults.
## Phase 8 - Registry And Capability Catalog
- [ ] Build a `CapabilityRegistry` projection from OpenHuman registries.
- OpenHuman files: `agent_registry`, `tools`, `mcp_registry`, `inference`,
`cost`, `approval`, `security`, controller registry in `src/core/all.rs`.
- TinyAgents components: `CapabilityRegistry`, `ComponentId`,
`ComponentKind`, model catalog, graph/tool/agent/store/middleware entries.
- Acceptance: OpenHuman models, tools, agents, graphs, stores, and middleware
can be looked up through one policy-aware capability projection.
- [ ] Add registry diagnostics for duplicate names and unsafe aliases.
- OpenHuman files: `src/openhuman/tools/generated.rs`,
`mcp_registry`, `composio`, `agent_registry`.
- TinyAgents components: component names, `ToolRegistry`, diagnostics.
- Acceptance: duplicate tool names, provider-specific aliases, MCP names, and
generated tool names fail closed before model dispatch.
- [ ] Use TinyAgents model catalog shape for local provider catalog snapshots.
- OpenHuman files: `docs/inference-provider-catalog.md`,
`src/openhuman/inference/presets.rs`,
`src/openhuman/inference/provider/factory.rs`.
- TinyAgents components: registry model catalog, local snapshots, price and
context metadata.
- Acceptance: model picker, router, budget estimator, and capability filter
read one normalized catalog projection.
## Phase 9 - Memory, Retrieval, Embeddings, Context, And Cache
- [ ] Adapt OpenHuman memory/retrieval to TinyAgents retriever interfaces.
- OpenHuman files: `memory`, `memory_search`, `memory_tree`,
`agent_memory/memory_loader.rs`, `context/*`.
- TinyAgents components: `EmbeddingModel`, `Retriever`, `VectorStore`,
`ScoredDoc`, context events.
- Acceptance: the agent harness can load retrieval context through a
TinyAgents retriever facade while OpenHuman stores remain authoritative.
- [ ] Move context compaction provenance into TinyAgents events.
- OpenHuman files: `context/README.md`, `tinyagents/summarize.rs`,
`tinyagents/payload_summarizer.rs`.
- TinyAgents components: `SummaryRecord`, `Compressed` events,
`PromptCacheLayout`, cache layout events.
- Acceptance: every summary records source ids, before/after token estimates,
policy version, and whether stable prompt prefix was preserved.
- [ ] Normalize embedding usage/cost records.
- OpenHuman files: `embeddings`, `memory_sync`, `cost`.
- TinyAgents components: embedding usage fields, model catalog pricing.
- Acceptance: embedding calls contribute usage and cost with provider/model,
dimensions, vector count, and source.
## Phase 10 - Dead Code And TinyAgents Re-Expression Audit
Do not delete these blindly. Treat this as an audit list for code that is dead,
vestigial, or generic runtime behavior now expressible through TinyAgents
harness/graph primitives. Delete only after call-site search, compatibility
assessment, and migration coverage are complete.
- [ ] Audit `src/openhuman/agent/harness/engine/*`.
- Current role: surviving seams from the retired in-house turn loops:
`CheckpointStrategy`, `ProgressReporter`, and `TurnProgress`.
- TinyAgents expression: `RunPolicy`, `AgentEvent`, `GraphEvent`,
`EventSink`, `HarnessStatusStore`, cap/stop middleware.
- Candidate outcome: move max-iteration and progress projection into
TinyAgents middleware/events, then delete `engine/*` if no product-specific
compatibility seam remains.
- [ ] Audit `src/openhuman/agent/harness/agent_graph.rs` and built-in
`agent_registry/agents/*/graph.rs` default selectors.
- Current role: per-agent graph hook, but most built-ins return
`AgentGraph::Default`.
- TinyAgents expression: a registry of `CompiledGraph`/graph factories keyed
by agent id, with default graph supplied by the runtime and custom graphs
registered only where they differ.
- Candidate outcome: replace dozens of boilerplate default `graph.rs` files
with registry defaults, keeping files only for agents with custom graphs.
- [x] Audit stale architecture references to removed in-house graph/loop code.
- Current files: `gitbooks/developing/architecture/agent-harness.md`,
`src/openhuman/context/README.md`.
- Candidate stale names: `src/openhuman/agent_graph/`, `GraphBlueprint`,
`run_turn_engine`, `run_tool_call_loop`, `harness/tool_loop.rs`, old
context summarizer files.
- TinyAgents expression: link to live `src/openhuman/tinyagents/*`,
`GraphBuilder`, `AgentHarness`, `ContextCompressionMiddleware`, and
graph-export/status surfaces.
- Candidate outcome: move historical details into a short "pre-migration
history" appendix or remove them from active architecture docs.
- **Partial:** flagged the `agent-harness.md` `agent_graph`/`GraphBlueprint`
section as HISTORICAL-removed (strong inline callout pointing at the live
tinyagents surfaces); fixed `context/README.md` "Used by" line that still
referenced the deleted `reduce_before_call`/`ProviderSummarizer`/
`SegmentRecapSummarizer`/`unified_compaction_enabled`. **Sweep completed:**
every code doc-comment that described `run_turn_engine`/`run_tool_call_loop`/
`tool_loop.rs` as *current* behavior (≈20 sites across 15 files: tools,
security, tokenjuice, triage, orchestration steering, task-local contexts,
cron, host_runtime, event-bus example, test-file headers) now points at the
live tinyagents surfaces; intentionally-historical "legacy X was removed /
parity with" notes were kept. Domain READMEs (tools, tokenjuice, approval)
fixed too — the tokenjuice one now records that `compact_tool_output` lost
its only production caller with the retired loop (re-wiring it as an
`after_tool` middleware; the stale default-Full wrapper is now deleted.
- [ ] Audit `src/openhuman/context/{pipeline,guard,microcompact}.rs`.
- Current role: context stats/session-memory bookkeeping plus older
compaction concepts; live history reduction moved to
`ContextCompressionMiddleware` and `MessageTrimMiddleware`.
- TinyAgents expression: context-window middleware, `Compressed` events,
`PromptCacheLayout`, cache layout events, `SummaryRecord`,
usage/context pressure status.
- Candidate outcome: keep only stats/session-memory state that remains
OpenHuman-specific; move compression policy/provenance into TinyAgents
middleware and delete unused reduction paths.
- [ ] Audit `src/openhuman/tinyagents/payload_summarizer.rs`.
- Current role: oversized tool-result compression via a `summarizer`
sub-agent with a local circuit breaker.
- TinyAgents expression: `ToolMiddleware::after_tool`,
`ContextCompressionMiddleware`, `SummaryRecord`, tool artifact/result
compaction events.
- Candidate outcome: convert to middleware over TinyAgents tool results so
the summarizer is no longer a separate OpenHuman-only hook.
- [ ] Audit `src/openhuman/agent_orchestration/running_subagents.rs`.
- Current role: bespoke live-task registry layered on TinyAgents
`InMemoryTaskStore`, plus watch channels, abort handles, tombstones,
task/session lookup, wait, steer, and cancel operations.
- TinyAgents expression: `TaskStore`, `SteeringCommand`, `SteeringHandle`,
`CancellationToken`, run tree/status store, durable OpenHuman ledger
projections.
- Candidate outcome: keep OpenHuman durable session/worker-thread records,
but collapse transient lifecycle mechanics into TinyAgents task/status
primitives once they can represent wait/steer/hard-abort needs.
- [ ] Audit `src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs`
after the graph-tool migration.
- Current role: validation, preflight, fanout, worktree setup, event
projection, overlap detection, result formatting.
- TinyAgents expression: graph nodes (`validate`, `dispatch`, `worker`,
`collect`, `finalize`) with `Send` fanout and reducers.
- Candidate outcome: keep a thin tool wrapper that invokes the graph and
formats the existing JSON payload; move orchestration mechanics into the
graph module.
- [ ] Audit hand-rolled `join_all` fanouts that are workflow orchestration, not
simple IO batching.
- Candidate files from current search: `src/openhuman/mcp_registry/registry.rs`,
`src/openhuman/learning/reflection.rs`,
`src/openhuman/inference/local/service/ollama_admin/diagnostics.rs`.
- TinyAgents expression: only migrate fanouts that need agent/run lineage,
checkpointing, policy, cancellation, or graph observability. Leave simple
independent IO probes as ordinary Rust concurrency.
- Candidate outcome: document why each fanout stays as `join_all` or move it
to `graph::parallel::map_reduce` / graph `Send`.
- [x] Audit tool registry comments and docs that still describe retired
direct-loop behavior.
- Current files: `src/openhuman/tools/traits.rs`,
`src/openhuman/tools/README.md`,
`src/openhuman/agent/harness/session/turn/tools.rs`.
- TinyAgents expression: `ToolRegistry`, `ToolMiddleware`, graph tool nodes,
tool safety metadata, `ToolExecutionContext`.
- Candidate outcome: update comments to describe the TinyAgents execution
path and delete references to the retired serial `harness::tool_loop`
dispatcher once no code path uses it.
- **Done:** `tools/traits.rs` concurrency note and `tools/README.md` "Used
by" now describe the tinyagents execution path (`SharedToolAdapter` /
`ToolPolicyMiddleware`); `session/turn/tools.rs` had no stale references.
## Phase 11 - Testing And Conformance
- [ ] Create a focused parity test matrix for the current TinyAgents route.
- OpenHuman files: `src/openhuman/tinyagents/tests.rs`,
`tests/agent_harness_e2e.rs`, `tests/agent_tool_loop_raw_coverage_e2e.rs`.
- TinyAgents components: `AgentHarness`, `AgentEvent`, `ToolRegistry`,
`ModelRegistry`, `MessageTrimMiddleware`, `ContextCompressionMiddleware`.
- Acceptance: chat turn, channel turn, sub-agent turn, unknown tool recovery,
approval denial, streaming text, streaming tool args, reasoning deltas,
early-exit pause, model-call cap, and cost footer all have tests on the
TinyAgents path.
- [x] Add a TinyAgents adapter inventory test.
- OpenHuman files: `src/openhuman/tinyagents/mod.rs`,
`src/openhuman/tinyagents/tests.rs`.
- Acceptance: one test asserts that the shared runner registers model, tools,
middleware, event bridge, context compression, stop hooks, and unknown-tool
policy in the intended order.
- **Done:** harness assembly extracted from `run_turn_via_tinyagents_shared`
into a testable `assemble_turn_harness` returning `AssembledTurnHarness`
(harness + cursor/error-slot/halt-summary/outcome-sink/steering/early-exit
seams). `adapter_inventory_registers_model_tools_and_middleware` asserts
model registry, callable tools + unknown-tool policy, 9 lifecycle + 2
around-tool middlewares, steering handle, early-exit hook;
`adapter_inventory_gates_context_middleware_on_window` proves the
compression/trim gating. SDK gap: `MiddlewareStack` exposes lengths but not
names, so exact ordering is documented at the registration sites and
guarded by counts.
- [ ] Port behavior clusters to TinyAgents testkit.
- OpenHuman files: `src/openhuman/agent/harness/*_tests.rs`,
`tests/agent_*`.
- TinyAgents components: harness `testkit`, graph `assert_graph`,
`GraphEventRecorder`, mock models/tools.
- Acceptance: legacy assertions over loop wording are replaced by assertions
over TinyAgents events, checkpoints, graph metadata, and final transcript.
- [ ] Add cross-module e2e tests for graph/sub-agent/model/tool composition.
- Candidate tests: workflow run with child sub-agents, delegation with review
loop, council fanout, MCP tool call, Composio approval denial, memory
retrieval plus summarization.
- Acceptance: tests cover default features, `openai`-feature compatible code
paths where available, and all-features where dependency constraints allow.
- [ ] Add fuzz-style graph composition tests.
- TinyAgents components: graph testkit, reducers, command routing, subgraph
nodes, sub-agent fake nodes.
- Acceptance: generated small graphs cover direct edges, conditional routes,
`Send` fanout, joins, interrupts, recursion caps, and checkpoint resume.
## Suggested Execution Order
1. Confirm version/feature compatibility and SDK-extension gaps.
2. Move tool safety/approval/security into TinyAgents middleware.
3. Normalize usage/cost records and parent/child rollups.
4. Persist TinyAgents event/status journals to the OpenHuman run ledger.
5. Build the OpenHuman `CapabilityRegistry` projection.
6. Convert one high-value built-in agent to a bespoke TinyAgents graph.
7. Adapt memory/retrieval/context surfaces where the TinyAgents interfaces are
a good fit.
8. Audit and remove or re-express dead/vestigial runtime code through
TinyAgents harness/graph primitives.
9. Finish with parity, adapter-inventory, conformance, e2e, and fuzz-style tests.
## Non-Goals For The First Migration Wave
- Do not replace OpenHuman's durable run ledgers with TinyAgents SQLite storage
until the one-time
transcript/session migration, TinyAgents store/status adapter, and
restart/resume parity tests are complete.
- Do not expose TinyAgents' OpenAI provider directly to product code while
OpenHuman provider config, credentials, OAuth, and billing classification are
still the product source of truth.
- Do not remove `DomainEvent` until all existing subscribers have a TinyAgents
event/status replacement.
-360
View File
@@ -1,360 +0,0 @@
# TinyAgents SDK Gaps
This document lists TinyAgents SDK features that are missing or only partially
available from the perspective of migrating OpenHuman's Rust agent core onto
TinyAgents.
Scope:
- Original source baseline: local TinyAgents checkout at `6f898fb`.
- Refresh note: TinyAgents 1.3.0 was re-verified from the published crate
source. Several older "missing" items are now shipped in 1.2.0-1.3.0:
tool policy metadata, recoverable unknown-tool calls, reasoning/tool-call
stream deltas, orchestration task stores, budget reservation/reconciliation,
ordered parallel map/reduce, sub-agent steering/task controls, workspace
isolation hooks, and middleware control outcomes. The backlog below keeps only
the residual OpenHuman migration pressure and SDK surface gaps.
- OpenHuman evidence: `src/openhuman/tinyagents/*`,
`src/openhuman/agent/*`, `src/openhuman/cost/*`, and
`src/openhuman/tokenjuice/*`.
- This is not the OpenHuman migration plan. That plan lives in
`docs/tinyagents-migration-spec.md`.
- Items here are upstream TinyAgents implementation candidates.
- Tests should follow each migrated surface; only broad end-to-end parity suites
should wait for the final cutover.
## Executive Summary
TinyAgents already has strong primitives for harness runs, graph execution,
middleware, event streams, model profiles, usage/cost accounting, checkpointers,
policy metadata, recoverable tool-call behavior, workspace isolation, and
sub-agent orchestration. The biggest remaining gaps are now narrower:
OpenHuman adapter migration, transcript/session migration, richer replay and
redaction rules, model/provider catalog integration, registry diagnostics, and a
few still-missing SDK fields.
OpenHuman can migrate more of `src/openhuman/agent/` by adopting the newer
TinyAgents surfaces and filling the remaining gaps:
- A free-form metadata map on `ToolSchema` for model-visible schema
annotations; SDK-owned enforcement metadata now lives in `ToolPolicy`.
- A reasoning field on the middleware-facing `harness::model::ModelDelta`;
`AgentEvent::ModelDelta` already carries nested `MessageDelta.reasoning`.
- A one-time migration path from old OpenHuman `session_raw/*.jsonl` and
Markdown transcripts into TinyAgents store/journal/status records.
- Production replay rules over TinyAgents stores/status: redaction, cursors,
backfill, cancellation, and OpenHuman controller compatibility.
- Storage compatibility options for SQLite users that already own a connection,
schema, or native sqlite patch policy.
- A `root_run_id` field on harness `RunConfig`; lineage exists in graph and
observability records, but not on the harness config itself.
- A money/USD field on `Usage`; token usage and cost totals remain separate.
- Provider/model catalog metadata that can drive preflight, fallback, and
reconciliation.
- Conformance suites for providers, tools, middleware, graph stores, and
checkpointers.
## Backlog
### 1. Rich Tool Policy Metadata
Status: shipped in 1.3.0; residual schema metadata gap remains.
TinyAgents now has SDK-owned `ToolPolicy`, `ToolSideEffects`, `ToolRuntime`,
`ToolAccess`, `WorkspaceAccess`, `SandboxMode`, `Tool::policy`, registry policy
snapshots, and `ToolPolicyMiddleware`. Strict policy can fail closed on
unclassified tools, enforce sandbox and result-byte requirements, and keep plain
`ToolSchema` as the model-visible projection.
Residual:
- `ToolSchema` still has no free-form metadata map for model-visible schema
annotations or app-specific hints. Use `ToolPolicy` for enforcement metadata
and keep this as the remaining SDK shape gap.
- OpenHuman still needs to map domain tool registry metadata into
`Tool::policy` snapshots before deleting adapter-local policy plumbing.
### 2. Recoverable Unknown Tool Calls
Status: shipped in 1.3.0.
TinyAgents now has `UnknownToolPolicy::{Fail, ReturnToolError, Rewrite}` and
emits `AgentEvent::UnknownToolCall` with the original requested name and
arguments. This distinguishes "tool not found" from "tool executed and failed"
and lets a run keep going so the model can correct itself.
OpenHuman follow-up:
- Replace the adapter-local `__openhuman_unknown_tool__` sentinel with
`UnknownToolPolicy` once the surrounding compatibility surface is migrated.
### 3. Reasoning And Tool-Argument Streaming
Status: shipped in 1.3.0; residual middleware event-shape gap remains.
TinyAgents `MessageDelta` now carries provider-neutral `text`, `reasoning`, and
`tool_call` channels, and `AgentEvent::ModelDelta` carries the delta with an
explicit `run_id` and `call_id`.
Residual:
- The middleware-facing `harness::model::ModelDelta` still has no `reasoning`
field, and the agent loop drops reasoning when converting stream items into
that middleware shape. Event consumers can read reasoning from
`AgentEvent::ModelDelta.delta.reasoning`.
- OpenHuman still needs to route provider reasoning/tool-argument deltas through
the TinyAgents stream before deleting UI-specific forwarding shims.
### 4. Durable Orchestration Task Store
Status: shipped as SDK primitives in 1.3.0; OpenHuman migration remains.
TinyAgents now has `TaskStore`, `InMemoryTaskStore`, `JsonlTaskStore`,
`OrchestrationTaskRecord`, lifecycle transitions, cancel/kill outcomes,
filters, graph/harness status with lineage, `graph::subagent_node`, and
`graph::subgraph`.
OpenHuman follow-up:
- Map durable sub-agent session rows and worker-thread records into TinyAgents
task/status/journal records while preserving controller compatibility.
- Retire bespoke task status/tombstone persistence in `running_subagents.rs`
only after restart/replay behavior is projected through the SDK records.
### 5. SQLite Storage Compatibility
Status: partially present.
TinyAgents 1.3 has `SqliteCheckpointer`, `from_connection`, and `schema_sql`,
and OpenHuman now enables the `sqlite` feature by aligning both Cargo worlds on
`rusqlite 0.40` / `libsqlite3-sys 0.38`. The remaining gap is not feature
enablement or basic schema access; it is ownership. OpenHuman still patches the
sqlite crates locally for the current toolchain, owns existing session/checkpoint
tables through `SqlRunLedgerCheckpointer`, and needs a clean way to adopt or
bridge SDK checkpoint storage without surrendering dependency or schema control.
Implement one or more OpenHuman compatibility paths:
- Provide a version-flexible storage layer, possibly via `sqlx` or a separate
crate feature matrix.
- Expose a small `CheckpointStore` persistence trait below `Checkpointer`.
- Add an adapter/cutover path that can project OpenHuman run-ledger checkpoints
into SDK checkpoint storage without breaking existing resume semantics.
Acceptance criteria:
- Applications that already own SQLite can use TinyAgents durable checkpoints
without native-link conflicts.
- OpenHuman can replace `SqlRunLedgerCheckpointer` with an SDK-supported adapter
or a thin schema integration.
- Storage features remain opt-in and keep the default crate dependency-light.
### 6. Production Event And Status Journals
Status: partially present.
TinyAgents has `HarnessEventJournal`, `StoreEventJournal`, `HarnessStatusStore`,
and `HarnessRunStatus`. OpenHuman still bridges TinyAgents events into its own
progress system, cost tracker, run ledger, and UI status stream.
Implement:
- Durable event journals with cursors, replay windows, filters, compaction, and
redaction hooks.
- Status stores with parent/root lineage, thread-scoped listing, phase details,
active tool/model call ids, usage totals, cost totals, and terminal summaries.
- Event filters for UI surfaces: text stream only, tool timeline, cost updates,
graph lifecycle, errors, task lifecycle.
- Redaction policies for prompts, tool args, tool results, PII, secrets, and
provider payloads.
- Stable event ids and offset semantics across process restarts.
Acceptance criteria:
- A UI can attach late and reconstruct a run without subscribing at start time.
- A supervisor can query every active descendant of a root run.
- OpenHuman event bridges become mostly format adapters, not state owners.
### 7. Cost, Usage, And Budget Enforcement
Status: shipped in 1.3.0; residual money field gap remains.
TinyAgents now has `Usage`, `UsageTotals`, `CostTotals`,
`BudgetLimits.max_cached_input_tokens`, budget middleware, and
`AgentEvent::{BudgetReserved, BudgetReconciled, BudgetWarning, BudgetExceeded}`.
The SDK can preflight, reserve, enforce, and reconcile token budgets.
Residual:
- `Usage` still has no USD/money field. Token usage and money remain separate
(`Usage`/`UsageTotals` vs. `CostTotals`), so OpenHuman cost UI still needs a
projection that joins token usage with pricing/cost records.
### 8. Model Catalog And Provider Resolution
Status: partially present.
TinyAgents has `ModelProfile`, including provider, model, modalities, tool
calling, streaming, structured output, reasoning, and token windows. OpenHuman
still has provider catalog logic and local model capability inference that drive
fallback, token budgeting, and routing.
Implement:
- SDK-owned model catalog snapshots with provider, model id, display name,
lifecycle status, context windows, modalities, streaming support, reasoning,
structured-output support, and pricing keys.
- Capability-driven model resolution: required capabilities, fallback chains,
local/cloud preferences, and provider health.
- Runtime profile discovery hooks for local models.
- Pricing table integration that maps `ModelProfile` to `CostTotals`.
Acceptance criteria:
- Model selection can be expressed in TinyAgents policy instead of
OpenHuman-only routing code.
- Fallback can reject models that lack required tool, vision, structured-output,
context-window, or reasoning capabilities.
- Token budgeting can use the resolved model's real context window.
### 9. Dynamic Tool Exposure And Allowlist Policy
Status: partially present.
TinyAgents can run with a provided tool registry, but OpenHuman needs per-agent,
per-tier, per-sub-agent, and per-task allowlists. Tool visibility depends on
security tier, workspace roots, parent/child delegation policy, model
capabilities, and whether the run is background or interactive.
Implement:
- A tool selection middleware that receives run context, agent identity, task
kind, parent policy, and model profile.
- Allowlist/denylist composition with explicit inheritance rules.
- Explainable exposure decisions for audit/debugging.
- Fail-closed behavior when policy metadata is missing.
Acceptance criteria:
- Sub-agents inherit only the tools they are allowed to call.
- Tool exposure decisions are visible in run events or observations.
- OpenHuman can remove adapter-local allowlist enforcement from most call paths.
### 10. Graph Fanout And Parallel Agent Ergonomics
Status: map/reduce helper shipped in 1.2.1-1.3.0; OpenHuman builder migration
remains.
TinyAgents now has ordered `map_reduce`, `FailurePolicy`, `ParallelOptions`,
max concurrency, per-item timeout, total timeout, and cooperative cancellation.
OpenHuman council runs and `spawn_parallel_agents` can use this helper directly.
Residual:
- The higher-level parallel-agent builder remains OpenHuman-specific policy
glue: task validation, `Send` dispatch shape, result envelopes, usage/cost
merging, and ownership/worktree policy adapters.
### 11. Sub-Agent Steering, Waiting, And Reuse
Status: SDK primitives shipped in 1.3.0; OpenHuman lifecycle projection remains.
TinyAgents now has sub-agent sessions/tools, steering, task stores, cancel/kill
control outcomes, graph/harness lineage, and reusable child-run primitives.
OpenHuman follow-up:
- Project existing detached run tracking, wait handles, user-facing
cancellation, early-exit handling, and parent-child progress aggregation onto
TinyAgents task/status records before reducing `running_subagents.rs`.
### 12. Workspace Isolation And Sandbox Hooks
Status: shipped in 1.3.0; OpenHuman policy integration remains.
TinyAgents now has `WorkspaceDescriptor`, `WorkspaceIsolation`,
`SharedRootWorkspace`, sandbox descriptors, `ToolExecutionContext.workspace`,
and `WorkspaceDescriptor::enforce(path, events)` which emits
`AgentEvent::WorkspaceViolation` and fails closed when a path leaves the allowed
roots.
OpenHuman follow-up:
- Implement OpenHuman's action-root, trusted-root, internal-workspace, worktree,
sandbox, and command-tier policy as a `WorkspaceIsolation` provider and tool
middleware projection.
### 13. Middleware Control Outcomes
Status: shipped in 1.3.0.
TinyAgents now has `MiddlewareControl::{StopWithFinal, Interrupt}`,
`RunContext::request_control`, precedence handling via
`MiddlewareControl::precedence()`, stable `kind()` labels, and
`AgentEvent::ControlApplied` so control decisions are visible in journals.
OpenHuman follow-up:
- Route early-exit tools and budget stop hooks through `MiddlewareControl`
before deleting adapter-local steering side channels.
### 15. Registry Diagnostics And Introspection
Status: partially present.
TinyAgents has registry primitives. OpenHuman still needs richer diagnostics for
duplicate components, alias resolution, component health, model/provider/tool
capabilities, and event listener wiring.
Implement:
- Registry snapshot export with models, tools, middleware, graph nodes,
checkpointers, task stores, event listeners, and aliases.
- Duplicate and shadowing diagnostics.
- Health/status probes for registered providers and stores.
- Machine-readable component dependency graph.
- Optional DOT/JSON graph export for runtime components, not only graph nodes.
Acceptance criteria:
- A CLI or UI can show exactly what TinyAgents components are active.
- Registry failures are actionable without inspecting app-specific logs.
- OpenHuman dead-code audits can map old modules to SDK-owned registry entries.
### 17. Storage And Graph Conformance
Status: partially present.
TinyAgents 1.3 includes storage conformance coverage for built-in checkpointers
and task stores, including SQLite under the feature. Durable OpenHuman adapters
and fuller graph behavior are still hard to migrate safely without shared
contract coverage.
Implement:
- Checkpointer conformance for OpenHuman adapters and caller-supplied stores.
- TaskStore conformance for lifecycle transitions, filters, cancellation,
timeout, kill, restart/replay, and concurrent writes.
- Graph conformance for `Send`, reducers, interrupts, resume, max concurrency,
dynamic routing, fanout failure policy, and deterministic result collection.
Acceptance criteria:
- Storage adapters can be swapped without changing graph behavior.
- Durable interrupt/resume semantics are proven across backends.
- Parallel-agent helpers have regression tests for order, failure, timeout, and
cancellation.
## Implementation Order
1. Define API contracts for tool policy, unknown-tool handling, streaming delta
channels, durable task storage, storage adapters, and control outcomes.
2. Implement the lowest-level data types and traits behind non-breaking
defaults.
3. Add in-memory implementations first.
4. Add durable stores and compatibility adapters second.
5. Add middleware helpers and high-level graph helpers.
6. Migrate OpenHuman adapters to the new SDK surfaces.
7. Remove OpenHuman-specific compatibility shims once the SDK behavior is
equivalent.
8. Implement conformance and regression tests last.
-246
View File
@@ -1,246 +0,0 @@
# TinyAgents Session Migration Design
Date: 2026-07-01
Status: Phase 1 implemented (2026-07-01) in `src/openhuman/session_import/`
as the `openhuman.session_import_run` controller
(`openhuman-core session-import run`). Phases 24 (read-side shadow, cutover,
retirement) are not started. Implementation deviations from the original
sketch are marked "as built" below.
Goal: a one-time, idempotent migration of persisted OpenHuman session data —
transcript JSONL, legacy Markdown transcripts, and run-ledger/sub-agent rows —
into TinyAgents store/journal records, so new internals can read by TinyAgents
`thread_id` / `run_id` / stream offset while legacy surfaces keep answering by
OpenHuman session key.
## Source inventory (what exists on disk today)
All facts verified against the current checkout (TinyAgents 1.5.0 pinned with the
`sqlite` feature enabled).
### 1. Transcript JSONL (source of truth)
- Path: `{workspace}/session_raw/{stem}.jsonl`
(writer/reader: `src/openhuman/agent/harness/session/transcript.rs`).
- Legacy layout still readable: `session_raw/{DDMMYYYY}/{stem}.jsonl`
(pre-0.53.4 date folders). A layout migration already exists
(`session/migration.rs`, marker `state/migrations/session_layout_v1.done`).
- Stem encodes identity and lineage:
- root session: `{unix_ts}_{agent_id}`;
- sub-agent: `{parent_chain}__{unix_ts}_{agent_id}` — the `__` chain is the
only parent/child link on disk; there is no pointer file.
- Line 1 is a `_meta` header (`MetaPayload`): `agent`, optional `agent_id` /
`agent_type` / `provider` / `model` / `thread_id` / `task_id`, `dispatcher`,
`created`, `updated`, `turn_count`, `input_tokens`, `output_tokens`,
`cached_input_tokens`, `charged_amount_usd`.
- Remaining lines are `MessageLine`s: required `role` + `content`; optional
`id`, `extra_metadata`, `provider`, `model`, `usage` (`input`, `output`,
`cached_input`, `context_window`, `cost_usd`), `reasoning_content`,
`tool_calls` (`id`, `name`, `arguments` as raw JSON string, optional
`extra_content`), `iteration`, `ts` (RFC-3339). Only the last assistant
message of each turn carries the per-turn fields.
- Tool-call encoding varies by the `_meta.dispatcher` value:
- native: structured `tool_calls` array on the assistant line;
- XML / P-format: markup (`<tool_call>…</tool_call>`, `name[a|b]`) embedded
verbatim in `content` — never re-parsed on resume today.
### 2. Markdown transcripts
- Human-readable companion: `{workspace}/sessions/{YYYY_MM_DD}/{stem}.md`
(legacy `sessions/{DDMMYYYY}/`). Never read back except by the one-release
legacy reader `read_transcript_legacy_md()` (HTML-comment `<!--MSG …-->`
format). Treat `.md` as a **fallback source only** when a stem has no JSONL.
### 3. Run ledger (SQLite `{workspace}/session_db/sessions.db`)
- `agent_runs`: id, kind (subagent | worker_thread | background_agent |
team_member | workflow_child), parent_run_id, parent_thread_id, agent_id,
status, worker_thread_id, task ids, checkpoint refs, metadata, timestamps.
- `run_events` (`run_id` + `sequence``event_type`, `payload_json`) and
`run_telemetry` (per-run token/cost roll-up).
- `graph_checkpoints` (written by `SqlRunLedgerCheckpointer`,
`src/openhuman/tinyagents/checkpoint.rs`): seq, thread_id, checkpoint_id,
run_id, record_json (a full tinyagents `Checkpoint<State>`), created_at.
- **No table stores the transcript stem/path.** Ledger row ↔ transcript file
correlation is by convention via `thread_id` (+ `task_id`).
### 4. Durable sub-agent sessions (JSON blob)
- `{workspace}/.openhuman/subagent_sessions.json`: a single pretty-printed
`Vec<DurableSubagentSession>``subagentSessionId`, `parentSession`
(parent session_key), `parentThreadId`, `workerThreadId`, `agentId`,
toolkit/model/sandbox/action-root selector fields, `status`, `reusable`,
inline `latestHistory` message mirror, timestamps.
## Target shape (TinyAgents 1.3+ primitives)
Use the crate's `harness::store` as the substrate — no new storage layer:
- `AppendStore` / `JsonlAppendStore`: one line per `StoreRecord { offset,
value, created_at_ms }`, offset = line index. Streams live under a store
root as `<stream>.jsonl`.
- `Store` / `FileStore`: key-value records as `<namespace>/<key>.json`.
Layout as built under `{workspace}/tinyagents_store/` (`kv/` holds the
`FileStore`, `journal/` the `JsonlAppendStore`). Two constraints reshaped the
original sketch:
- TinyAgents store/stream names are **slash-free** (ASCII alphanumerics plus
`-_.` — the crate's path-traversal guard), so the `thread/{id}/messages`
shape is impossible; names are dot-separated.
- Journals are **per session, not per thread**: multiple transcript files can
share one `_meta.thread_id`, and appending them into a shared stream would
interleave sessions. The descriptor carries `thread_id`, so thread-level
views remain a projection.
| Record | Primitive | Stream / key |
| ----------------------------- | ------------- | ------------------------------------------------ |
| Message journal (per session) | `AppendStore` | stream `session.{session_key}.messages` |
| Session descriptor | `Store` | ns `sessions`, key `{session_key}` |
| Item idempotency ledger | `Store` | ns `migration_items`, key `sha256(source path)` |
| Global run marker | `Store` | ns `migrations`, key `session_import_v1` |
Run event journals and run descriptors were dropped from v1 (see the resolved
open questions at the end): run events stay queryable in SQLite and belong to
the P2 journal-canonicalization work.
Descriptor records carry the compatibility mapping both directions:
```json
// ns sessions, key {session_key} (session_key = transcript stem)
{
"session_key": "1719800000_orchestrator",
"parent_session_key": null, // from the __ stem chain
"thread_id": "…", // _meta.thread_id or imported-{stem}
"thread_id_synthesized": false,
"task_id": "…", // from _meta.task_id (nullable)
"run_ids": ["…"], // joined from agent_runs via thread_id
"stream": "session.1719800000_orchestrator.messages",
"dispatcher": "native",
"agent_name": "…", "agent_id": "…", "agent_type": "…",
"provider": "…", "model": "…",
"created": "…", "updated": "…", "turn_count": 1,
"usage": { "input": 0, "output": 0, "cached_input": 0, "cost_usd": 0.0 },
"source": { "jsonl": "session_raw/….jsonl", "md": null },
"import": { "version": 1, "imported_at": "…", "warnings": 0 }
}
```
Message journal values (as built) are full-fidelity records of what
`read_transcript()` returns — `{id?, role, content, extra_metadata?}`, where
`extra_metadata` carries the reconstructed `openhuman_turn_usage` block
(`iteration`, `reasoning_content`, per-turn `usage`, `tool_calls` including
`extra_content`). `ChatMessage` itself marks `id`/`extra_metadata`
`skip_serializing`, so the journal defines its own record type
(`JournalMessage`). Projection into the tinyagents `harness::message::Message`
model is left to the read side.
### Lineage keys
- `thread_id`: taken from `_meta.thread_id` when present; when absent (old
files), synthesize `imported:{session_key}` so every migrated session has a
stable thread stream. Record the synthesis in the descriptor.
- Parent/child: derive from the `__` stem chain and cross-check against
`agent_runs.parent_thread_id` / `subagent_sessions.json`; disagreements are
warnings, stem chain wins (it is the write-time truth).
- `root_run_id`: tinyagents carries it in graph types but OpenHuman's
`graph_checkpoints` schema drops it. The importer sets `root_run_id` on run
descriptors by walking `agent_runs.parent_run_id` to the root. (Separately,
adding a `root_run_id` column to `graph_checkpoints` is a small schema
follow-up — tracked in the audit, not part of this migration.)
## Tool-call normalization
- Native-dispatcher transcripts: `tool_calls` arrays map 1:1 onto tinyagents
`ToolCall` records (`arguments` parsed from the raw JSON string; parse
failure → keep as string + warning).
- XML / P-format transcripts: **do not re-parse in v1.** The markup stays
verbatim in message content, exactly as the live resume path treats it
today; the descriptor records `"dispatcher": "xml" | "pformat"` so a later
pass (or read-side shim) can re-extract structured calls using the existing
`ToolDispatcher` parsers if ever needed. Re-parsing at import time is high
risk (P-format needs the positional-arg registry of the tool set as it
existed then) for no current consumer.
## Idempotency and observability
Follow the proven `session_layout_v1` pattern, plus per-item ledger entries:
- Global marker: `Store` record `migrations/session_import_v1` with run
timestamp, counters, and tool version. Present → skip scan entirely
(bypassed by `--only`, `--force`, and dry runs).
- Per-source ledger: each imported stem writes
`migration_items/{sha256(workspace-relative source path)}` with source size
+ mtime. Re-runs (e.g. after a crash) skip completed items; a changed
size/mtime re-imports and overwrites that item's records (only that
session's stream file is reset and rewritten).
- Never mutate or delete sources. `session_raw/`, `sessions/`, `sessions.db`,
and `subagent_sessions.json` remain untouched; legacy readers keep working
until parity is proven.
- Surface (as built): the `openhuman.session_import_run` controller —
`openhuman-core session-import run` / RPC — with `dry_run`, `only`
(stem glob), `force`, `verbose`, and `workspace` (dir override) params.
- dry-run prints the per-file plan (stem → thread stream, message count,
dialect, warnings) and writes nothing;
- real run emits a summary: files scanned / imported / skipped / failed,
messages written, warnings list; grep-friendly `[session-import]` log
prefix on every line.
- Failure policy: per-file errors are warnings (matching
`migrate_session_layout_if_needed`); the command never aborts the batch and
never blocks core startup — it is an explicit command, not a boot hook, in
v1. Wiring it into startup comes only after parity tests.
## Fixture matrix (required before implementation is "done")
Implemented in `src/openhuman/session_import/ops_tests.rs` (18 tests covering
every row below, plus dry-run purity and sources-untouched assertions).
Golden-file tests over real captured shapes:
1. Current flat `session_raw/{ts}_{agent}.jsonl`, native dispatcher.
2. Legacy date-folder `session_raw/{DDMMYYYY}/…` (pre-layout-migration).
3. Legacy Markdown-only session (no JSONL twin) via the `<!--MSG-->` reader.
4. Sub-agent stems, including a two-level `a__b__c` chain.
5. XML-dialect transcript (markup-in-content preserved byte-for-byte).
6. P-format transcript (ditto).
7. Assistant line with `tool_calls` incl. `extra_content` (Gemini
thought-signature passthrough).
8. Malformed files: missing `_meta` first line, truncated last line, empty
file, unparseable message line (skip + warning, matching the current
reader's tolerance).
9. `_meta` without `thread_id` (synthesized thread id path).
10. Ledger cross-check: `agent_runs` row whose `parent_thread_id` disagrees
with the stem chain (warning path).
11. Idempotency: import → re-run (all skipped) → touch one source → only that
item re-imports.
Parity assertion: for every fixture, reading the migrated thread stream back
and projecting it into `ChatMessage` history must equal what
`read_transcript()` returns for the source file (same messages, same
`openhuman_turn_usage` reconstruction).
## Phasing
1. **Importer + CLI + fixtures** — done (`src/openhuman/session_import/`):
write-only into `tinyagents_store/`, sources untouched, nothing reads the
new records yet.
2. **Read-side shadow**: run-inspection surfaces read both and diff-log
mismatches (behind a debug flag).
3. **Cutover**: new internals read TinyAgents records; legacy readers stay as
compatibility projections keyed by `session_key`.
4. **Retirement**: delete legacy readers once telemetry shows no shadow
mismatches (separate decision, out of scope here).
## Open questions (resolved in v1)
- `run_events` / `run_telemetry` journaling: **not in v1** — transcripts +
descriptors only. Run events stay queryable in SQLite; P2 (journal
canonicalization) owns that surface.
- Store root: **`{workspace}/tinyagents_store/`** (`kv/` + `journal/`).
Living under the workspace means the fail-closed
`is_workspace_internal_path` guard keeps agent tools from writing here —
desirable.
- `subagent_sessions.json` `latestHistory` mirrors: **dropped** — they are a
cache of the same messages the child's own transcript carries; the child
stem imports as its own session.
@@ -1,118 +0,0 @@
# 00 — Harness Audit: what remains in OpenHuman, what can migrate
Ground truth as of 2026-07-04, main + waves 12 (#4473, #4483).
Scope totals (incl. tests):
| Tree | LOC | Notes |
| ---- | --- | ----- |
| `src/openhuman/agent/` | ~56,951 | this audit's core scope |
| `src/openhuman/agent_orchestration/` | ~25,749 | lifecycle glue over SDK graphs |
| `src/openhuman/tinyagents/` | ~12,185 | the live adapter seam (all turns route here) |
| `src/openhuman/inference/` | ~52,955 | provider layer; does NOT wrap tinyagents |
Classification legend: **(a)** thin adapter over tinyagents · **(b)**
migratable/duplicative generic harness logic · **(c)** product policy, stays ·
**(d)** unclear.
## 1. Where execution already lives on tinyagents
Chat turns (`harness/session/turn/core.rs``turn/graph.rs`), channel/CLI
turns (`harness/graph.rs`, entry `agent/bus.rs` `agent.run_turn`), and
sub-agent turns (`harness/subagent_runner/ops/graph.rs`) all route through
`run_turn_via_tinyagents_shared` in the `src/openhuman/tinyagents/` adapter.
Middleware, stop hooks, unknown-tool policy, and context compression are on
SDK middleware. The legacy `run_tool_call_loop` is deleted.
Direct `tinyagents::` references inside `agent/` are sparse by design — the
adapter module owns the seam. Highest densities: `subagent_runner/ops/runner.rs`
and `ops/graph.rs` (15 refs each), `turn/core.rs` (13).
## 2. Remaining subsystems, ranked by migratable size
1. **Session/transcript shell — `harness/session/`, ~13.2k LOC.** Transcript
read/write + dialect-suffix compat (`transcript.rs` 1347+978 tests),
builder assembly (`builder/factory.rs` 1312 — product, stays), turn IO
(`session_io.rs`), migration (373), progress projection
(`tool_progress.rs` 252). Duplicates crate `Store`/`AppendStore`/
`StoreChatHistory`. Old-plan C1 covers the cutover; dual-writes are ON,
shadow reads landed (flag OFF). **Biggest single unlock (~911k incl.
session_db/subagent_sessions/session_import).**
2. **Sub-agent runner — `harness/subagent_runner/`, ~6.1k LOC.** Prepare →
filter tools → run child → checkpoint → extract → mirror. Duplicates
`SubAgent`/`SubAgentSession`/`SubAgentTool` + `graph::subagent_node`.
`extract_tool.rs` (612) + `handoff.rs` (287) are generic (old-plan C5/C6);
`tool_prep.rs` allowlist half is product.
3. **Orchestration lifecycle glue — `agent_orchestration/`, ~3.7k of it.**
`running_subagents.rs` **1931** (target ≤300; watch channels, abort/wait/
steer, tombstones — belongs on a durable `TaskStore`, see 06),
`spawn_parallel_graph.rs` 1764 (dup of `map_reduce`).
4. **Progress/streaming projection — ~2.9k LOC.** `progress.rs` (303,
`AgentProgress` incl. `ThinkingDelta`/`SubagentThinkingDelta`/
`ToolCallArgsDelta`), `session/tool_progress.rs` (252, bridges
`ProviderDelta``AgentProgress`), `progress_tracing.rs` + `langfuse.rs`
+ tests (~2k). Duplicates crate `AgentEvent`/`HarnessEventJournal`/
Langfuse exporters. Deletion gated on old-plan C4 journal parity **and**
on crate streaming fixes (docs 02/03 here) so nothing regresses in the UI.
5. **Tool-call dialects — ~1.9k LOC.** `dispatcher.rs` (609) `ToolDispatcher`
trait (native/XML/P-Format), `harness/parse.rs` (833) permissive XML/JSON
parser with arg-key drift recovery, `pformat.rs` (499) compact positional
`name[a|b]` calls (~80% token cut, OpenHuman-invented). Live path is
native; these survive for prompt-guided dialects + transcript compat.
Extraction candidate (06) so the C1 read-cutover becomes a pure delete.
6. **Multimodal assembly — `multimodal.rs` 1690 (+1020 tests).**
`[IMAGE:]`/`[FILE:]` markers → provider content blocks, mime allowlist,
PDF extraction, fetch gating, truncation budgets. ~1550 generic (06).
7. **Cost — `cost.rs` 343.** Per-turn USD + tier pricing + budget stop-hook
feed. Blocked on crate `Usage` having no money field and
`reasoning_tokens` never being populated (see 01/02). Old-plan C3 flip
criteria stand.
8. **Tool policy/filter — `tool_policy.rs` 524 + `harness/tool_filter.rs`
299.** Overlaps crate `ToolPolicy`; residual gap is free-form
model-visible `ToolSchema` metadata (sdk-gaps §1).
## 3. Confirmed product — keep in OpenHuman
- `triage/` (3.8k) — trigger classification; *invokes* the migrated loop.
~800-line generic evaluator core is an extraction candidate (06), envelope/
escalation/events stay.
- `task_dispatcher/` (1.6k) — deterministic card dispatch (CAS claim →
autonomous turn). Mechanics shrink onto `graph::todos` (old-plan C2).
- `archivist/` (1.4k + 1k tests) — episodic indexing/lessons; memory policy.
- `prompts/` (~4k) — section assembly/rendering; product voice.
- `agent/tools/` (~4k) — product tools (`run_workflow`, preferences,
`delegate_to_personality`, todo CRUD → C2 projections).
- `builder/factory.rs`, definitions (`definition.rs` 846,
`definition_loader.rs`, `builtin_definitions.rs`), `memory_context.rs`,
`memory_protocol.rs`, credentials/sandbox context, `debug/`, `library/`,
`bus.rs`, `schemas.rs`.
- `inference/` (53k) — stays for credential ownership, billing
classification, OAuth, local/Ollama. Largest *parallel* duplication of a
tinyagents capability in the tree, but per the standing verdict
(`reliable.rs` 900) not forced; revisit only after a native Anthropic
provider (05) proves out.
## 4. Streaming & reasoning today (OpenHuman side)
Provider deltas (`inference::provider::ProviderDelta::{TextDelta,
ThinkingDelta, ToolCallArgsDelta}`) are bridged in
`session/tool_progress.rs:226` into `AgentProgress`, a per-request
`mpsc::Sender` channel distinct from the DomainEvent bus. Reasoning reaches
the UI **only** through this OpenHuman-side path (plus the `ThinkingForwarder`
shim), because the crate drops reasoning (01 §3). Fixing 02/03 in the vendor
crate lets `AgentProgress` become a thin projection of crate `AgentEvent`s
and unblocks deleting `ThinkingForwarder`, `tool_progress.rs`, and eventually
the `progress_tracing` stack.
## 5. Delta vs the old plan's inventory
The C0C7 verdicts hold. New/raised items from this sweep:
- `running_subagents.rs` keeps growing (1931; was flagged at the same size on
2026-07-03 — enforce the ≤300 target when C6 lands or it accretes further).
- The dialect layer's delete gate ("no live path parses provider text") is
reachable only after C1 phase 3 **and** either upstreaming P-Format/XML
parsing (06) or accepting native-only transcripts.
- `inference/provider/reliable.rs` verdict unchanged, but 05 (native
Anthropic in-crate) is the first concrete step that could eventually make
routing non-turn provider calls through the crate harness attractive.
@@ -1,144 +0,0 @@
# 01 — TinyAgents Crate Audit (vendor/tinyagents @ 1.5.0 + ac73382)
~41.5k non-test LOC (~60k with tests); 54 integration tests, 15 examples.
Deps: tokio (sync/time/macros), reqwest (rustls/json/stream), serde, sha2;
optional `rusqlite` (`sqlite`), `rhai` (`repl`). No provider-specific SDKs.
The crate's own `goal.md` is a pre-written gap list from OpenHuman's
migration perspective and corroborates most findings below.
## 1. Module map
- `harness/` — the agent runtime. Largest: `middleware/` (~5.3k, trait +
~18 built-ins incl. retry/timeout/fallback/budget/tool-policy/contextual
selection/approval/redaction/prompt-cache guard), `providers/` (~3k:
`MockModel` + one real `OpenAiModel`), `agent_loop/` (~2.3k), `model/`
(~1.9k: `ChatModel`, `ModelRequest/Response`, `ModelStream`,
`StreamAccumulator`, `ModelRegistry`), `observability/` (~1.8k journals/
status/Langfuse), `subagent/` (~1.2k), `events/` (~1.2k), plus store/tool/
summarization/embeddings/steering/cache/usage/cost/no_progress/testkit.
- `graph/` (~19k) — durable typed graphs: compiled runtime, checkpoints
(memory/file/sqlite), orchestration `TaskStore` + tools, `todos`, `goals`,
`map_reduce`, subagent/subgraph nodes, recursion, export, testkit.
- `registry/`, `language/` (.rag), `repl/` (.ragsh, feature-gated).
- Ergonomics gap: graph surface is fully re-exported at crate root, but core
harness types (`AgentHarness`, `ChatModel`, `ModelRequest`, `OpenAiModel`,
middleware, message types) require deep `harness::…` paths (`lib.rs`).
## 2. The harness loop (what's good)
`AgentHarness::run_loop` (`harness/agent_loop/mod.rs:293-673`): cancellation
checkpoint → steering drain → deadline/call caps → build request →
`before_model` → wrap-onion model call with retry + fallback chain
(`:766-859`) + per-call `tokio::time::timeout` budget (`:902-917`) + response
cache (`:685-756`) → `after_model` → usage/events → `MiddlewareControl`
(StopWithFinal/Interrupt) → tools → repeat. Unknown-tool policy
`Fail | ReturnToolError | Rewrite` (`:563-618`). Sub-agents nest as tools
with a depth cap. Rich extension points; the loop *shape* itself is fixed.
## 3. Defects & gaps (file:line), grouped
### Reasoning / thought tokens — scaffolding exists, last mile unwired
- `ContentBlock` = `Text | Json | Image | ProviderExtension`
(`harness/message/types.rs:21-30`) — **no Thinking/Redacted variant**; no
persisted home for Anthropic thinking blocks or their signatures.
- OpenAI SSE path hardcodes `reasoning: String::new()` per delta
(`providers/openai/mod.rs:772`); never parses `reasoning_content` or
o-series reasoning.
- `StreamAccumulator::finish` **drops accumulated reasoning** — final message
built from text + tool chunks only (`model/mod.rs:683-710`).
- Loop's middleware-facing `ModelDelta` built with content + tool_call only,
dropping reasoning (`agent_loop/mod.rs:980-984`) — the exact sdk-gaps §3
item blocking `ThinkingForwarder` deletion.
- `Usage.reasoning_tokens` (`usage/types.rs:29`) and
`CostTotals.reasoning_cost` (`cost/mod.rs:69`) exist but are **always 0**:
`convert_usage` maps only prompt/completion/total/cached
(`openai/mod.rs:708-719`); `completion_tokens_details.reasoning_tokens`
isn't even in the wire struct (`openai/types.rs:235-256`).
- No signature/redacted_thinking handling anywhere → correct multi-turn
extended-thinking + tool-use vs native Anthropic is currently impossible.
### Streaming — real SSE, but observation-only and single-level
- OpenAI streaming is genuine incremental SSE (`openai/mod.rs:1020-1073`,
line buffering `:874-896`, chunk folding `:753-804`); tool-call arg
fragments stream as `ToolCallDelta` (`:796-799`).
- **`invoke_streaming` returns a completed `AgentRun`, not a `Stream`**
(`agent_loop/mod.rs:195-241`); deltas reachable only via `EventSink`/
middleware (`:929-994`).
- **Sub-agents never stream**: `SubAgentTool::call`
`SubAgent::invoke` non-streaming path (`subagent/mod.rs:535`, `:179-189`).
No parent/root attribution on deltas.
- No explicit tool-call started/completed stream events.
- `MockModel::stream` replays a completed `invoke` (`model/types.rs:536-549`)
— mock-backed tests aren't truly incremental.
### Performance — correctness-first cloning in the hot loop
- Full history cloned per turn: `ModelRequest::new(messages.clone())`
(`agent_loop/mod.rs:356`); full request cloned again **per retry/fallback
attempt** (`:804`, `:937`).
- Tool schemas rebuilt every iteration: `.with_tools(self.tools.schemas())`
(`:356`).
- Provider `translate_request` re-clones every message + tool schema and
re-serializes tool args per call (`openai/mod.rs:404-449, 570-584`).
- Response-cache key = serde round-trip + canonicalize + SHA-256 over the
**entire request** per cached call (`cache/mod.rs:101-106`).
- `StreamAccumulator::push_tool_chunk` linear scan per fragment
(`model/mod.rs:643-650`).
- Tools execute **strictly serially** (`agent_loop/mod.rs:526-659`);
`parallel_tool_calls` is a capability flag only.
### Providers — one adapter, many presets
- Only `OpenAiModel` is real; `anthropic()`/`deepseek`/`groq`/`xai`/
`openrouter`/`together`/`mistral`/`ollama` are base-URL presets on the
Chat Completions wire (`openai/mod.rs:309-383`).
- **No Anthropic Messages API**: zero `cache_control`/`ephemeral` hits in
providers; no thinking config; `Usage.cache_creation_tokens` always 0
(only `cache_read_tokens` from `prompt_tokens_details.cached_tokens`,
`openai/mod.rs:713-716`).
- Prompt-cache shaping is metadata-only: `cache_segments` /
`prompt_fingerprint` / `cacheable_prefix_ids()`
(`model/types.rs:385-405`) + `PromptCacheGuardMiddleware`
(`middleware/mod.rs:19-60`) never reach any wire field — the spec's
"extreme prompt caching" (`docs/spec/README.md:258-278`) is observability,
not request shaping.
- Spec explicitly plans feature-flagged native `openai`/`anthropic`/`ollama`
(`docs/spec/README.md:279-284`; slot documented at
`providers/types.rs:322-337`).
### Durability / storage
- `TaskStore` is in-memory/JSONL only; no lifecycle history/replay
(goal.md §4/§11) — what `running_subagents.rs` (1931 LOC) needs to shrink.
- SQLite ownership: bundled `rusqlite 0.40` vs app-owned sqlite → needs a
trait-first/connection-adapter path (goal.md §5).
- `Store` has no compare-and-set (single-writer constraint documented in the
old plan).
## 4. Test coverage holes
No tests for: native Anthropic/prompt caching/thinking (no impl), reasoning
end-to-end (only hand-fed accumulator test,
`tests/e2e_reasoning_and_selection.rs:57-98`), parallel tool execution,
caller-consumable streaming, sub-agent stream propagation. Live-provider
tests exist behind env keys; conformance suites exist for graph/checkpoint.
## 5. Ranked improvement opportunities (effort: S≈1-2d, M≈3-5d, L≈1-2w, XL multi-week)
1. **Reasoning end-to-end** — L — doc 02. Unblocks `ThinkingForwarder`
deletion + real reasoning cost accounting.
2. **Hot-loop de-cloning + cache-key + schema caching** — M — doc 04.
Biggest runtime win for OpenHuman's long transcripts.
3. **Streaming out of the harness + up from sub-agents** — L — doc 03.
Unblocks `tool_progress.rs`/progress projection deletions (C4).
4. **Native Anthropic Messages provider + cache_control shaping** — L→XL —
doc 05. Highest-leverage provider gap.
5. **Parallel tool execution** — M — doc 04 §4.
6. **Usage completeness (OpenAI reasoning tokens) + catalog pricing** — S→M
— doc 02 §5; feeds old-plan C3 budget flip.
7. **Durable TaskStore (SQLite/JSONL lifecycle + replay)** — L — doc 06 §4.
8. **SQLite dependency flexibility** — M — goal.md §5.
9. **Crate-root re-export ergonomics** — S.
@@ -1,88 +0,0 @@
# 02 — Thought Tokens End-to-End (vendor crate work)
Goal: reasoning/thinking content becomes a first-class, streamed, persisted,
replayed, and priced citizen of the crate — so OpenHuman deletes
`ThinkingForwarder` and stops re-projecting thinking via its own
`ProviderDelta::ThinkingDelta` bridge (`session/tool_progress.rs:226`).
All steps are vendor-crate changes (`vendor/tinyagents`), committed on a
submodule feature branch, gitlink-bumped into OpenHuman, PR'd upstream.
Precedent: `NoProgressTracker` (#7), MicrocompactMiddleware (`ac73382`).
## Step 1 — Message representation
- Add `ContentBlock::Thinking { text: String, signature: Option<String> }`
and `ContentBlock::RedactedThinking { data: String }` to
`harness/message/types.rs:21-30`.
- Serde: additively tagged; existing transcripts (no thinking blocks) parse
unchanged. Round-trip tests in `message/test.rs`.
- `AssistantMessage` rendering helpers must skip thinking blocks for
plain-text extraction (`Message::text()`-style accessors) but preserve them
for provider replay.
## Step 2 — Accumulator persistence
- `StreamAccumulator::finish` (`harness/model/mod.rs:683-710`): emit the
accumulated `self.reasoning` as a leading `ContentBlock::Thinking` on the
final message instead of dropping it. Carry signature fragments (Anthropic
`signature_delta`) through a new accumulator field.
- Keep the existing `reasoning()` side-channel accessor for backward compat.
## Step 3 — Delta plumbing
- Populate `MessageDelta.reasoning` from providers:
- OpenAI path: parse `delta.reasoning_content` (DeepSeek/compat) and
o-series reasoning summaries instead of hardcoding `String::new()`
(`providers/openai/mod.rs:772`); add the wire fields to
`openai/types.rs`.
- Anthropic path (05): `thinking_delta` / `signature_delta` events.
- Thread reasoning into the middleware-facing `ModelDelta`
(`agent_loop/mod.rs:980-984`) — closes sdk-gaps §3. Add
`ToolDelta.tool_name` on the first fragment (tool-name-on-start), the
other half of that gap.
- Emit reasoning in `AgentEvent::ModelDelta` with parent/root run
attribution (pairs with doc 03's sub-agent propagation so
`SubagentThinkingDelta` can become a projection).
## Step 4 — Replay correctness (Anthropic contract)
Anthropic requires thinking blocks (with signatures) to be replayed verbatim
in the assistant turn preceding tool results. With Step 1 the blocks live in
the transcript; the provider translation (05) must:
- Serialize `Thinking`/`RedactedThinking` blocks back onto the wire for
assistant messages in multi-turn tool-use conversations.
- Never send thinking blocks to providers that reject them (OpenAI compat
path strips them) — provider capability flag `supports_thinking` on
`ModelProfile`.
- Property test: build a 3-turn tool-use conversation with thinking, assert
byte-stable signature replay.
## Step 5 — Usage & cost
- Add `completion_tokens_details.reasoning_tokens` to the OpenAI wire struct
(`openai/types.rs:235-256`) and map it in `convert_usage`
(`openai/mod.rs:708-719`).
- Anthropic (05): thinking output tokens are billed as output; keep
`reasoning_tokens` as the *reported* subset where the API exposes it.
- `CostTotals.reasoning_cost` (`cost/mod.rs:69`) then prices real numbers —
feeds OpenHuman's C3 budget-flip criteria (pricing table wired).
## Step 6 — OpenHuman follow-through (after gitlink bump)
1. Delete `ThinkingForwarder` (old-plan C7 item; sdk-gaps §3 closes).
2. `AgentProgress::ThinkingDelta`/`SubagentThinkingDelta` become projections
of crate `AgentEvent::ModelDelta.reasoning` — shrinks
`session/tool_progress.rs` toward deletion (with doc 03).
3. Persisted-transcript compat: OpenHuman's `multimodal.rs` reasoning-block
handling shrinks once the crate owns thinking blocks in messages.
## Tests (crate)
- Provider-level: SSE fixture streams with reasoning deltas → accumulator →
message contains `Thinking` block; usage carries reasoning tokens.
- Loop-level: middleware `on_model_delta` sees reasoning; `AgentRun.messages`
persists it; replay serialization per provider capability.
- Extend `tests/e2e_reasoning_and_selection.rs` beyond hand-fed deltas.
Effort: **L** (12 weeks) crate-side; OpenHuman follow-through **S**.
@@ -1,80 +0,0 @@
# 03 — Streaming Improvements (vendor crate work)
Goal: the harness exposes a real caller-consumable stream, sub-agent deltas
propagate to the parent with lineage attribution, and tool calls get explicit
lifecycle events — so OpenHuman's `AgentProgress` layer becomes a thin
projection and the C4 progress_tracing deletion has full-fidelity input.
## Current state (from 01)
- Provider SSE is genuinely incremental (`openai/mod.rs:1020-1073`) and
tool-call arg fragments stream (`ToolCallDelta`, `:796-799`).
- But `invoke_streaming` returns a completed `AgentRun`
(`agent_loop/mod.rs:195-241`); consumers must attach an `EventSink` or
middleware to see deltas.
- Sub-agents run the non-streaming path (`subagent/mod.rs:535`, `:179-189`)
— a child's tokens/thinking/tool activity are invisible until it returns.
- No tool-call started/completed events; `MockModel::stream` replays a
completed response (`model/types.rs:536-549`).
## Step 1 — `invoke_stream`: a Stream-returning entry point
- New API: `AgentHarness::invoke_stream(...) ->
(impl Stream<Item = AgentStreamItem>, JoinHandle<Result<AgentRun>>)` (or a
single stream whose terminal item carries the `AgentRun`).
- `AgentStreamItem` (new enum, `harness/stream/`): `TurnStarted`,
`ModelDelta(ModelDelta)` (text + reasoning + tool_call, post doc 02),
`ToolCallStarted { call_id, tool_name }`, `ToolCallArgsDelta`,
`ToolCallCompleted { call_id, outcome }`, `AssistantMessage`,
`UsageUpdated`, `RunCompleted/Failed` — each stamped with
`run_id / parent_run_id / root_run_id / depth`.
- Implementation: a bounded `mpsc` fed from the existing emit points in
`invoke_model_streaming_once` (`agent_loop/mod.rs:929-994`) and the tool
execution section (`:526-659`). The `EventSink` path stays; this is a
convenience projection over the same events, not a parallel system.
- Backpressure: bounded channel + documented drop-oldest vs block policy
(default: block; the loop already awaits network).
## Step 2 — Sub-agent delta propagation
- `SubAgentTool::call` switches to the streaming child path and forwards
child `AgentStreamItem`s into the parent's stream/sink with the child's
`run_id` + inherited `root_run_id` and `depth+1`
(`subagent/mod.rs:535` → `SubAgent::invoke_stream`).
- Recursion-safe: items flow through the parent's channel; no unbounded
fan-in (children are executed serially today; revisit with doc 04 §4).
- Closes goal.md §3's "attribute every delta to parent/root run id".
## Step 3 — Tool-call lifecycle events
- Emit `ToolCallStarted` as soon as the terminal `Completed` (or the first
named `ToolCallDelta`, with doc 02's tool-name-on-start) identifies the
tool — this is what OpenHuman's UI needs to show "running X…" before args
finish streaming.
- Emit `ToolCallCompleted` with outcome + duration after the wrap-onion tool
call returns.
## Step 4 — Test/mock fidelity
- `MockModel::stream` gains scripted incremental emission (list of
`ModelStreamItem`s with optional delays) so streaming tests are truly
incremental (`model/types.rs:536-549`).
- Tests: caller consumes `invoke_stream` and observes interleaved model/tool
items; nested sub-agent test asserts child deltas arrive with correct
lineage before the parent's final message.
## Step 5 — OpenHuman follow-through
1. `run_turn_via_tinyagents_shared` adapters consume `invoke_stream` (or the
sink projection) and map 1:1 onto `AgentProgress` — deleting the
`ProviderDelta` bridge in `session/tool_progress.rs` (252).
2. `SubagentThinkingDelta`/subagent progress becomes lineage-filtered
projection — removes bespoke child-progress plumbing in
`subagent_runner/ops/runner.rs`.
3. C4 (journal-backed web progress) gains the missing fidelity: journals +
live stream share one event vocabulary, enabling the
`progress_tracing.rs` + `langfuse.rs` deletion (~2k).
Effort: **L** crate-side (Steps 14), **M** OpenHuman follow-through.
Depends on: doc 02 for reasoning-in-deltas (can land in either order; both
touch `invoke_model_streaming_once`, so sequence the merges).
@@ -1,93 +0,0 @@
# 04 — Performance (vendor crate work)
Goal: remove O(history) per-turn overhead from the agent loop. OpenHuman
transcripts are long (multi-hundred-message sessions with large tool
results); every listed cost below scales with transcript size and is paid
one or more times **per model call**.
## 1. Stop cloning the transcript per turn/attempt — biggest win
Today:
- `ModelRequest::new(messages.clone())` per iteration
(`agent_loop/mod.rs:356`).
- `model.invoke(state, request.clone())` / `model.stream(state,
request.clone())` per retry/fallback **attempt** (`:804`, `:937`) — a
3-model fallback chain with 2 retries can clone the full history 6×.
Plan:
- Change `ModelRequest.messages` to `Arc<Vec<Message>>` (or `Arc<[Message]>`)
with copy-on-write semantics (`Arc::make_mut` on the rare mutation paths —
middleware that rewrites history). Public builder API stays source-
compatible via `impl Into<Arc<Vec<Message>>>`.
- Retry/fallback passes `&ModelRequest`; `ChatModel::invoke/stream` take
`&ModelRequest` (breaking trait change — acceptable pre-2.0, coordinated
with OpenHuman's adapter in the same gitlink bump).
- The loop appends assistant/tool messages to its own `Vec` and rebuilds the
`Arc` view per turn (one cheap `Arc::new` of a `Vec` it already owns, or
an immutable persistent-list structure if profiling justifies it — start
with `Arc<Vec>`, measure).
## 2. Cache tool schemas per run
`.with_tools(self.tools.schemas())` rebuilds every `ToolSchema` (cloning
name/description/`parameters: Value`) each iteration (`agent_loop/mod.rs:356`).
Tool sets are fixed for a run (dynamic-selection middleware operates on the
request afterward). Plan: compute `Arc<Vec<ToolSchema>>` once at run start;
middleware that filters tools clones only then (copy-on-filter).
## 3. Response-cache key without full serde round-trips
`cache_key` = `serde_json::to_value(request)` → canonicalize → `to_vec` →
SHA-256 over the entire request per cached call (`cache/mod.rs:101-106`).
Plan: maintain an incremental hash — hash each message once when appended
(messages are immutable after append), fold message digests + tool-schema
digest + params digest into the key. Falls out naturally once messages are
`Arc`'d (attach a lazily-computed digest per message).
## 4. Parallel tool execution
Tools run strictly serially (`agent_loop/mod.rs:526-659`) while
`parallel_tool_calls` exists only as a capability flag. Plan:
- Execute a turn's tool calls via `futures::stream::iter(...)
.buffer_unordered(cap)` but **emit results in request order** (index-sorted
reassembly) so transcripts stay deterministic.
- Default cap: 1 (today's behavior) — opt-in via
`AgentHarnessBuilder::with_tool_concurrency(n)` and a per-tool
`ToolPolicy::serial` escape hatch (side-effecting tools opt out).
- Middleware contract: `before_tool`/`after_tool` hooks fire per call; wrap
middleware must be `Send + Sync` (already is). Events carry call ids so
interleaving is attributable (pairs with doc 03 lifecycle events).
- OpenHuman: keep cap=1 initially; enable for read-only tool families after
the approval-gate interaction is reviewed (approval middleware must park
the whole batch, not one call).
## 5. Provider translation
`translate_request` re-clones all messages/tool schemas and re-serializes
tool args per call (`openai/mod.rs:404-449, 570-584`). Plan: with #1/#2 the
inputs are `Arc`'d; add a per-run memo of the translated static prefix
(system + tools JSON) keyed by the schema digest from #3. This also makes
prefix-stability for provider prompt caches (05) explicit rather than
incidental.
## 6. Minor
- `StreamAccumulator::push_tool_chunk` linear scan (`model/mod.rs:643-650`)
→ index map by `call_id` (only matters for many-tool turns; cheap fix).
- `InMemoryResponseCache` mutex per get/put (`cache/mod.rs:119-131`) — fine;
re-check only if #3 makes cache use hot.
## Measurement (gate for merging #1#3)
Add a criterion-style bench (or a `live_*`-excluded integration bench) in the
crate: synthetic 500-message transcript, 40 tools, 20-turn loop against
`MockModel` — assert allocations/turn and wall time before/after. OpenHuman
side: `[budget_shadow]`-style log of per-turn overhead in the adapter for one
release to confirm on real sessions.
Effort: #1 **M** (trait-breaking, coordinated bump), #2 **S**, #3 **SM**,
#4 **M**, #5 **S**. Order: #2/#3 first (non-breaking), then #1 (+#5), #4
independent.
@@ -1,83 +0,0 @@
# 05 — Native Anthropic Provider + Prompt-Cache Request Shaping
Goal: a feature-flagged native Anthropic Messages-API provider in the crate
(the spec already reserves the slot, `docs/spec/README.md:279-284`,
`providers/types.rs:322-337`), plus wiring the existing cache-segment
metadata to real wire fields. Highest-leverage provider gap: today
`anthropic()` is an OpenAI-compat preset that silently loses prompt caching,
thinking, and cache-write accounting.
## Why this matters to OpenHuman
- Claude-family models are primary; every turn without `cache_control`
breakpoints pays full input-token price on a growing transcript.
- Extended thinking + tool use cannot work correctly at all without
signature replay (see 02 §4).
- `Usage.cache_creation_tokens` is structurally present but always 0 —
OpenHuman's cost accounting (C3 flip) undercounts cache writes.
- OpenHuman's own `inference/` layer already handles this correctly; a
native crate provider is the precondition for ever routing provider calls
through the crate harness (the standing `reliable.rs` gate, old-plan 02.2).
## Step 1 — `providers/anthropic/` behind `feature = "anthropic"`
- Messages API: `system` as top-level blocks, `tools` array, `tool_choice`,
`max_tokens` required, `anthropic-version` header; reuse the crate's
reqwest/rustls stack. No new deps.
- Response mapping → `ModelResponse`: content blocks (text, tool_use,
thinking, redacted_thinking per doc 02), `stop_reason`, and usage incl.
`cache_creation_input_tokens` / `cache_read_input_tokens`
`Usage.cache_creation_tokens` / `cache_read_tokens`.
- SSE streaming: `message_start` / `content_block_start` /
`content_block_delta` (`text_delta`, `input_json_delta`,
`thinking_delta`, `signature_delta`) / `content_block_stop` /
`message_delta` — mapped onto `ModelStreamItem` incl. tool-name-on-start
(`content_block_start` carries the tool name — doc 02 §3 / doc 03 §3 get
this for free on Anthropic).
- `ProviderKind::Anthropic` switches from preset to native when the feature
is on; preset remains as fallback for compat gateways.
## Step 2 — `cache_control` shaping from `cache_segments`
`ModelRequest` already carries `cache_segments` / `prompt_fingerprint` /
`cacheable_prefix_ids()` (`model/types.rs:385-405`) — currently ignored by
translation. Plan:
- Translation places up to 4 `cache_control: {type: "ephemeral"}`
breakpoints at segment boundaries: tools block, system tail, and the two
highest-value conversation prefixes (mirrors OpenHuman's proven layout).
- `PromptCacheGuardMiddleware` (`middleware/mod.rs:19-60`) is promoted from
observer to enforcement partner: layout-change events now correspond to
actual cache invalidation, so its warnings become actionable.
- OpenAI path: no wire field needed (implicit prefix caching), but #04 §5's
stable-prefix memo guarantees byte-stable prefixes — document that as the
OpenAI half of "cache-aware shaping".
- TTL/beta options (e.g. 1h cache) via `provider_options` passthrough.
## Step 3 — Capability & catalog integration
- `ModelProfile` gains `supports_thinking`, `supports_cache_control`,
cache-write pricing multipliers; `ModelCatalog` entries for the Claude
family map cache read/write token prices into `CostTotals` (feeds
old-plan C3's "pricing table wired" criterion and sdk-gaps §7/§8).
## Step 4 — Tests
- Wire-fixture tests: request JSON golden files (system/tools/cache_control
placement, thinking replay with signatures); SSE fixture → accumulator →
message + usage assertions (cache read/write, thinking tokens).
- `live_anthropic.rs` behind `ANTHROPIC_API_KEY`: cache-hit-on-second-call,
multi-turn thinking + tool-use replay.
## Step 5 — OpenHuman follow-through (deliberately conservative)
- Do **not** immediately reroute OpenHuman's turn traffic — `inference/`
stays authoritative (credentials, billing classification, OAuth).
- First consumer: sub-agent/background/eval traffic behind a flag, comparing
cost + cache-hit telemetry against `inference/` for the same models.
- Re-evaluate the `reliable.rs` (900) verdict and old-plan 02.2 only after
divergence-free telemetry; that decision gets its own migration note.
Effort: **L→XL** (Step 12 ≈ 2 weeks incl. tests; Steps 35 incremental).
Depends on doc 02 (thinking blocks in `ContentBlock`) for full value; can
land Step 1 text/tool support before thinking if 02 lags.
@@ -1,98 +0,0 @@
# 06 — Upstream Extraction: OpenHuman logic to move INTO tinyagents
Extends the old plan's C5 batch with the vendor-submodule workflow: extract
generic harness logic into `vendor/tinyagents`, bump the gitlink, shrink the
local file to product residue, then delete. Two extractions already shipped
this way (`NoProgressTracker` → 1.5.0, MicrocompactMiddleware → `ac73382`,
upstream PR pending on `feat/microcompact-middleware`).
Ordered by value (generic-LOC reclaimed locally + capability gained by the
crate). Figures are non-test lines; tests roughly double each.
## 1. Tool-call dialect layer (~1,900) — `pformat.rs` + `dispatcher.rs` + `harness/parse.rs`
- Crate gains: a `ToolCallDialect` trait beside native tool calling —
P-Format compact positional calls (`name[a|b]`, ~80% token cut on
tool-heavy turns — a genuine crate selling point for small/local models
that lack native tool calling), permissive XML/JSON parsing with arg-key
drift recovery (`TOOL_ARG_KEYS`: arguments|args|parameters|params|input).
- Product residue: none identified — zero DomainEvent coupling.
- Unlock: old-plan C1 step 4 becomes a pure local delete once no live path
parses provider text (transcript compat reads move with it).
## 2. Multimodal attachment resolver (~1,550 of 1,690) — `multimodal.rs`
- Crate gains: marker → provider content-block resolution, mime allowlist,
PDF text extraction, fetch gating, truncation budgets.
- Product residue: the `[IMAGE:]`/`[FILE:]` marker convention itself.
- Note: land after doc 02 so thinking/reasoning block handling doesn't need
to be ported twice.
## 3. Overflow-to-artifact tool-result store (~500) — `tool_result_artifacts/`
- Already built on crate `Store`; overflow-to-artifact is a generic harness
concern (pairs with the crate's microcompact). Residue: PII scrub hook.
- Include `subagent_runner/extract_tool.rs` (612) + `handoff.rs` (287):
progressive-disclosure Q&A over handoff-cached oversized results — the
natural companion API.
## 4. Durable TaskStore upgrade + detached lifecycle (~crate work enabling a 1,631-line local delete)
- Crate gains: SQLite-backed `TaskStore` (behind the existing `sqlite`
feature) with lifecycle history, cancellation records, wait/kill/steer
handles, replay/listing by parent/root/thread (goal.md §4/§11).
- Unlock: `agent_orchestration/running_subagents.rs` 1931 → ≤300 (old-plan
C6.2) stops being an adapter squeeze and becomes a projection.
## 5. Hook trait machinery (~450) — `hooks.rs` + `stop_hooks.rs`
- Crate gains: `PostTurnHook`/`StopHook` traits + scheduling shell (incl.
the archivist's ~200-line hook-scheduling shell). Product hook bodies
(archivist, memory, cost stop-hooks) stay local.
## 6. Host runtime adapter core (~350) — `host_runtime.rs`
- Native/Docker `RuntimeAdapter` overlaps crate `WorkspaceIsolation`;
merge as backends of one crate abstraction.
## 7. Fuzzy tool ranker (~250) — `tool_filter.rs`
- Generic ranking into `ContextualToolSelectionMiddleware` (whose shadow
flip is old-plan C3.1); Composio input types stay product.
## 8. LLM triage node (~800 of 3,779) — `triage/` evaluator core
- Crate gains: a generic "classify with tiered model fallback + cache +
verdict parse" graph node. Envelope/escalation/events (Composio,
DomainEvents, agent ids) stay product.
## 9. Small fry
- `ArgRecoveryMiddleware` core (~150).
- Task-local context carriers (`fork_context`, `sandbox_context`,
`task_recency_context`, `turn_attachments_context`, ~500): not an
extraction — replace with typed fields on the crate execution context
(`ToolExecutionContext`/`RunContext`), old-plan C6.4. Requires a crate API
addition (generic `extensions: TypeMap` on the run context is the clean
form) — do that API change first, then delete all four locally.
## Workflow per extraction (checklist)
1. Branch in `vendor/tinyagents` (`feat/<name>`); port code + tests to crate
idioms (types.rs/test.rs split, ≤500-line files, module README).
2. `cargo fmt && cargo clippy --all-targets -- -D warnings && cargo test`
in the submodule.
3. Bump gitlink in OpenHuman on a paired branch; swap local callers to the
crate type; shrink local file to residue (or delete); run OpenHuman
suites per the two-branch CI model.
4. Tick `99-deletion-ledger.md` in the old plan folder (it remains the
master ledger) and note the extraction here.
5. Push submodule branch to `tinyhumansai/tinyagents`, open upstream PR.
Local gitlink may run ahead of upstream merge (already the case for
`ac73382`) — keep the queue shallow: ≤2 unmerged submodule branches.
## License note
tinyagents is GPL-3.0-only; OpenHuman consumes it in-tree already, so
extraction changes nothing legally — but extracted code becomes GPL. Flag
any file with third-party-licensed snippets before moving (none known).
@@ -1,80 +0,0 @@
# 07 — Execution Order, Gates, and Reclaim
This plan runs **alongside** the old plan's C0C7 (which continues:
wave 3 = flip `session_shadow_reads`, flip crate BudgetMiddleware to
enforcing, microcompact upstream PR). The vendor workstreams here are named
**V1V6** to avoid collision.
## Workstreams
| WS | Doc | Theme | Effort | Blocks / unblocks |
| -- | --- | ----- | ------ | ----------------- |
| V1 | 04 §23 | Non-breaking perf (schema cache, incremental cache key) | SM | none; immediate win |
| V2 | 02 | Reasoning end-to-end | L | deletes `ThinkingForwarder` (C7); feeds C3 pricing |
| V3 | 03 | invoke_stream + sub-agent propagation + tool lifecycle | L | C4 progress_tracing deletion; `tool_progress.rs` delete |
| V4 | 04 §1,4,5 | Arc'd messages (trait break), parallel tools, translation memo | M | coordinated gitlink bump with adapter changes |
| V5 | 05 | Native Anthropic + cache_control shaping | L→XL | needs V2 for thinking; enables 02.2/`reliable.rs` re-eval |
| V6 | 06 | Upstream extraction batch (dialects, multimodal, artifacts, TaskStore, hooks, ranker, triage node) | rolling | each unlocks a ledger delete; TaskStore item unlocks C6.2 |
## Suggested sequence
```
now ──► V1 (quick, non-breaking)
├─► V2 reasoning ──► V5 Anthropic (thinking replay) ─► 02.2 re-eval
├─► V3 streaming ──► C4 progress deletion
├─► V4 perf-breaking (single coordinated bump after V2/V3 merge)
└─► V6 extractions (continuous, 12 in flight, ≤2 unmerged submodule branches)
old plan in parallel: wave 3 (C1 read flip, C3 budget flip) — independent
```
Rationale:
- V1 first: zero-risk, benefits every live turn, no API changes.
- V2 before V5: thinking blocks must exist in `ContentBlock` before the
Anthropic adapter can replay them; V5 Step 1 (text/tools) may start early.
- V2 and V3 both touch `invoke_model_streaming_once` — land V2 first
(smaller diff), rebase V3.
- V4's `ChatModel` trait break rides one coordinated gitlink bump so
OpenHuman's adapter updates land atomically with it.
- C1 (sessions read flip) is **independent** of all V-work — don't serialize
behind it; but the dialect extraction (V6.1) should merge before C1
step 4's dispatcher/parse/pformat delete.
## Gates (each item ships only when its gate is green)
| Item | Gate |
| ---- | ---- |
| V2 merge | crate reasoning e2e tests + OpenHuman turn with thinking model shows deltas + persisted blocks + nonzero reasoning_tokens |
| ThinkingForwarder delete | V2 gitlink bump live in one release, no `[thinking]` divergence logs |
| V3 merge | nested sub-agent stream test green; `AgentProgress` projection parity vs current UI events (fixture diff) |
| progress_tracing delete | C4 journal parity AND V3 projection parity |
| V4 merge | crate bench: ≥50% allocation reduction on 500-msg synthetic; OpenHuman conformance suite green single-threaded |
| Parallel tools ON (cap>1) | approval-gate batch-parking reviewed; read-only tool families only |
| V5 first traffic | flag-gated non-turn traffic; cost/cache telemetry matches `inference/` within tolerance |
| Each V6 extraction | crate tests + local residue shrink + ledger tick + upstream PR opened |
## Expected reclaim (local, incl. tests; adds to old plan §5's ~29k)
| Source | Lines |
| ------ | ----- |
| `ThinkingForwarder` + `tool_progress.rs` + progress projection residue (V2+V3) | ~700 |
| progress_tracing stack (C4, unblocked by V3) | ~2,000 (already counted in old plan) |
| Dialect layer local delete (V6.1 + C1.4) | ~1,900+tests (counted in old plan C1) |
| `running_subagents.rs` squeeze via durable TaskStore (V6.4 + C6) | ~1,600 (counted in old plan C6) |
| Task-local context carriers via crate RunContext extensions (V6.9) | ~500 |
Net-new local reclaim beyond the old plan: **~12k lines** — the headline
value of V-work is not deletion but **capability and cost**: correct thought
tokens, live sub-agent streaming, prompt-cache dollars, and a faster loop on
every turn. The old plan's ~29k reclaim proceeds in parallel and several of
its gates (C4, C6.2, C1.4, C3 pricing, C7 ThinkingForwarder) become
*reachable* only because of V2/V3/V5/V6.
## Tracking
- Deletions continue to tick `../tinyagents-full-migration-plan/99-deletion-ledger.md`.
- Submodule branch queue + upstream PR status tracked in this folder's
README as extractions ship (keep ≤2 unmerged).
- Re-audit `docs/tinyagents-sdk-gaps.md` after V2/V3/V5 land — they close
gaps §3 (reasoning stream), part of §6 (event fidelity), §7 pricing
inputs, and the provider half of §8.
@@ -1,79 +0,0 @@
# TinyAgents Vendor Improvement & Harness Migration Plan
Status: new plan (2026-07-04). Branch: `docs/tinyagents-vendor-audit-plan`.
This folder is the successor planning surface to
[`../tinyagents-full-migration-plan/`](../tinyagents-full-migration-plan/)
(whose `CONTINUATION-2026-07.md` C0C7 workstreams remain the authoritative
*migration* backlog). What changed and why this folder exists:
1. **TinyAgents is now a vendored, editable submodule** (`vendor/tinyagents`,
pinned at `v1.5.0` + `ac73382` MicrocompactMiddleware, patched into both
Cargo worlds via `[patch.crates-io]`). Every "file an upstream issue and
wait" item in the old plan is now **direct work we can do in-tree**, test
against OpenHuman immediately, and PR upstream from the submodule.
2. A fresh ground-truth audit (2026-07-04, three parallel sweeps: harness
inventory, crate internals, docs status) surfaced **crate-side defects and
gaps that no prior doc captured** — dropped reasoning tokens, a
preset-only "Anthropic" provider, observability-only prompt caching, and
hot-loop cloning. Fixing these in the vendor crate multiplies the value of
every migration workstream.
## Folder map
| Doc | Theme |
| --- | ----- |
| [`00-audit-harness.md`](00-audit-harness.md) | What's left in `src/openhuman/agent/` (+ orchestration/inference), classified: adapter / migratable / product |
| [`01-audit-tinyagents.md`](01-audit-tinyagents.md) | Crate capability map + concrete defects found (file:line) |
| [`02-reasoning-thought-tokens.md`](02-reasoning-thought-tokens.md) | End-to-end thought-token plan: `ContentBlock::Thinking`, signatures, deltas, usage/cost |
| [`03-streaming.md`](03-streaming.md) | Stream-returning harness entry point, sub-agent delta propagation, tool-call lifecycle events |
| [`04-performance.md`](04-performance.md) | Clone elimination, schema caching, cache-key hashing, parallel tool execution |
| [`05-anthropic-prompt-caching.md`](05-anthropic-prompt-caching.md) | Native Anthropic Messages provider + `cache_control` request shaping |
| [`06-upstream-extraction.md`](06-upstream-extraction.md) | OpenHuman logic to move INTO tinyagents (extends old-plan C5) + deletions unlocked |
| [`07-execution-order.md`](07-execution-order.md) | Sequencing, gates, dependency graph vs C0C7, reclaim estimates |
## Headline findings (TL;DR)
**Migration headroom.** `src/openhuman/agent/` is ~57k LOC (+~26k in
`agent_orchestration/`, ~12k adapter in `src/openhuman/tinyagents/`). The turn
loop already runs on tinyagents (`run_turn_via_tinyagents_shared`); the
remaining deletable/migratable surface is ~29k LOC, dominated by the session/
transcript shell (~13k), sub-agent runner (~6k), orchestration lifecycle glue
(~3.7k), progress/tracing projection (~2.9k), and tool-call dialects (~1.9k).
See `00-audit-harness.md`.
**Crate defects that block deletions today** (all now fixable in-vendor):
- **Reasoning is dropped end-to-end**: the OpenAI SSE path hardcodes
`reasoning: ""`, `StreamAccumulator::finish` discards accumulated
reasoning, the loop's middleware `ModelDelta` omits it, and
`Usage.reasoning_tokens` is never populated. This is why OpenHuman's
`ThinkingForwarder` shim still exists.
- **"Anthropic" is an OpenAI-compat preset**, not a Messages-API adapter: no
`cache_control` prompt caching, no thinking blocks, no
`cache_creation_input_tokens` accounting.
- **Prompt-cache shaping is observability-only**: `cache_segments` /
`cacheable_prefix_ids()` never reach any wire field.
- **Hot-loop cloning**: full message history cloned per turn *and* per
retry/fallback attempt; tool schemas rebuilt per iteration; response-cache
key re-serializes the whole transcript per call.
- **No caller-facing stream**: `invoke_streaming` returns a completed run;
sub-agents never stream to parents; no tool-call start/complete events.
- **Tools execute strictly serially** despite a `parallel_tool_calls`
capability flag.
**Sequencing in one line**: fix reasoning + streaming + perf in the vendor
crate first (02/03/04 — they unblock `ThinkingForwarder` deletion and improve
every live turn immediately), land the Anthropic provider (05) next, and run
upstream extraction (06) in parallel with the old plan's C1 sessions cutover.
## Rules (inherited, unchanged)
- Approval/security/sandbox/workspace/credential boundaries are inviolate.
- JSON-RPC contracts stay stable unless a migration note lands with the change.
- Adapter first → proven parity → delete; deletions are mandatory and named.
- Vendor-crate changes: commit in `vendor/tinyagents` on a feature branch,
bump the gitlink in OpenHuman, PR the submodule diff upstream
(precedent: `NoProgressTracker` #7, MicrocompactMiddleware `ac73382`).
- Stage files explicitly (`git add <paths>`); verify branch before commit.
- Keep every doc in this folder ≤500 lines.
-115
View File
@@ -1,115 +0,0 @@
# Directory listing: batch follow data via GraphQL
**Status:** proposed (backend work) · **Owner:** Agent World / tiny.place
**Related:** `app/src/agentworld/pages/DirectorySection.tsx`, `src/openhuman/tinyplace/manifest.rs`
## Problem
The Directory page (`DirectorySection.tsx`) renders one card per registered agent.
Historically each card issued **two** REST/JSON-RPC calls on mount:
- `follows.stats(agentId)` — to show the follower count.
- `follows.followers(agentId, { limit: 100 })` — only to decide whether *the
current user* follows that agent.
For an `N`-agent directory that is `1 + 2N` requests on a single page load
(e.g. 50 agents → ~101 requests), which trips tiny.place rate limits.
## What already shipped (frontend-only mitigation)
`DirectorySection` now fetches the viewer's **following set once** via
`follows.following(myAgentId, { limit: 500 })` and derives each card's
follow-state locally (`useMyFollowing`). That removes the per-card
`followers` lookup → roughly halves the requests (`1 + N + 1`).
The per-card **follower count** (`follows.stats`) is still `N` requests because
`directory.listAgents()` does not return counts. Eliminating those needs the
backend change below.
## Proposed backend change
Add a single GraphQL query on the tiny.place backend that returns the directory
already joined with follow data, so the whole page is **one** request.
### GraphQL schema (tiny.place backend repo: `tinyhumansai/backend`)
```graphql
type DirectoryAgent {
agentId: String!
name: String
description: String
username: String
skills: [String!]
tags: [String!]
followerCount: Int! # aggregate from the follows table
isFollowedByViewer: Boolean! # viewer follows this agent (join on viewerAgentId)
}
type DirectoryAgentsResult {
agents: [DirectoryAgent!]!
count: Int!
}
extend type Query {
directoryAgents(
viewerAgentId: String # optional; null → isFollowedByViewer = false
limit: Int = 100
offset: Int = 0
): DirectoryAgentsResult!
}
```
`followerCount` should be a grouped aggregate (`COUNT(*) … GROUP BY followee`)
and `isFollowedByViewer` a left-join on `(follower = viewerAgentId, followee =
agentId)` — both computed in one query, no per-agent fan-out.
### Rust SDK passthrough (`tinyplace` crate)
Mirror the existing GraphQL methods (e.g. `ledger_transactions`):
```rust
// tinyplace SDK
pub async fn directory_agents(
&self,
params: Option<&DirectoryAgentsParams>,
) -> Result<DirectoryAgentsResult, Error> { /* GraphQL POST */ }
```
### OpenHuman core controller (`src/openhuman/tinyplace/manifest.rs`)
Add a handler alongside `handle_tinyplace_graphql_ledger_transactions`:
```rust
// method: "openhuman.tinyplace_graphql_directory_agents"
pub(crate) fn handle_tinyplace_graphql_directory_agents(
params: Map<String, Value>,
) -> ControllerFuture { /* deserialize params → client.directory_agents(...) → JSON */ }
```
Register it in the same controller table as the other `tinyplace_graphql_*`
methods, and add a `*_degrade` fallback (empty list) consistent with the
existing passthroughs.
### Frontend client (`app/src/lib/agentworld/invokeApiClient.ts`)
Add under the `graphql` namespace:
```ts
directoryAgents: (params?: { viewerAgentId?: string; limit?: number; offset?: number }) =>
call<DirectoryAgentsResult>('openhuman.tinyplace_graphql_directory_agents', { ...params }),
```
### Frontend page (`DirectorySection.tsx`)
Replace `directory.listAgents()` + `useMyFollowing` + per-card `follows.stats`
with a single `graphql.directoryAgents({ viewerAgentId: myAgentId })` call.
Each card then reads `followerCount` and `isFollowedByViewer` straight off the
row — **one** request for the whole page. Keep `follows.follow/unfollow` for
the optimistic toggle.
## Acceptance
- Loading an `N`-agent directory issues exactly **1** data request (plus wallet
identity), down from `1 + 2N`.
- Follower counts and follow-state match the current per-card behavior.
- Pagination supported via `limit`/`offset`.
+1 -1
View File
@@ -278,7 +278,7 @@ Memory encryption keys derive from user credentials via Argon2id, ensuring memor
- **Auth handoff**: Web-to-desktop authentication uses single-use login tokens with 5-minute TTL, exchanged via Rust HTTP client (bypasses CORS)
- **Network TLS**: All WebSocket and HTTP connections use rustls, no dependency on platform OpenSSL
- **State management**: Sensitive data lives in Redux (memory) and OS keychain (persistent). No localStorage for credentials or tokens
- **Prompt injection guard**: User prompts are normalized/scored and enforced server-side (`allow | review | block`) before model/tool execution. See [`docs/PROMPT_INJECTION_GUARD.md`](../../docs/PROMPT_INJECTION_GUARD.md)
- **Prompt injection guard**: User prompts are normalized/scored and enforced server-side (`allow | review | block`) before model/tool execution. See `src/openhuman/prompt_injection/`
---
@@ -57,8 +57,6 @@ OpenHuman pins `tinyagents = { version = "1.5.0", features = ["sqlite"] }` (see
- **`repl` / expressive-language features unused.** OpenHuman drives graphs from Rust (`GraphBuilder`), not the crate's `.rag` REPL language.
- **Adapter map (feature-gated SDK piece → OpenHuman replacement):** provider clients → `ProviderModel`; crate SQLite checkpointer rows not yet adopted → `SqlRunLedgerCheckpointer`; task/status stores not yet controller-canonical → OpenHuman SQL/JSON run ledgers (`running_subagents`, `workflow_runs`, `agent_teams`, `command_center`). The generic harness/graph/middleware/event primitives are used as-is.
Migration backlog and per-phase tasks live in [`docs/tinyagents-migration-spec.md`](../../../docs/tinyagents-migration-spec.md). The current harness inventory snapshot lives in [`docs/tinyagents-harness-migration-audit.md`](../../../docs/tinyagents-harness-migration-audit.md).
The agent harness is the runtime that turns a user message (or a webhook fire, or a cron tick) into a complete, tool-using LLM interaction. It owns the tool-call loop, sub-agent dispatch, the trigger-triage pipeline, and the hook surface around them. It does **not** own provider HTTP transport, tool implementations, prompt-section assembly, or memory storage - those are separate domains the harness composes.
This page walks through what happens in one turn, then zooms in on each of the moving parts.
@@ -458,7 +456,7 @@ Every agent turn — chat (`harness/session/turn/core.rs`), channel/CLI (`harnes
- **Sub-agent build pipeline** (`subagent_runner/`) — definition resolution, archetype tool filtering, provider resolution, narrow prompt building, memory context, worker-thread mirror, handoff cache, checkpoint/resume — stays openhuman-owned. Sub-agents already *execute* on the harness; the crate's generic `SubAgentTool` would discard this pipeline for marginal crate-native depth tracking (openhuman's `spawn_depth_context` already bounds recursion).
- **Durable run ledgers** (`workflow_runs`, `agent_teams`, `command_center`, `subagent_sessions`) stay on openhuman SQLite/JSON until their controller projections and restart semantics are mapped onto TinyAgents task/status/journal records. The `agent_teams` race-safe SQL compare-and-swap task claim remains OpenHuman-owned.
> **Note:** TinyAgents 1.3+ ships harness store/cache/session primitives (`harness::store` with JSONL append stores, `harness::cache`, `harness::subagent`, lineage-aware status) plus graph task stores and conformance contracts. The plan for migrating the session shell, sub-agent pipeline, and detached-task lifecycle onto those primitives lives in [`docs/tinyagents-harness-migration-audit.md`](../../../docs/tinyagents-harness-migration-audit.md).
> **Note:** TinyAgents 1.3+ ships harness store/cache/session primitives (`harness::store` with JSONL append stores, `harness::cache`, `harness::subagent`, lineage-aware status) plus graph task stores and conformance contracts. The session shell, sub-agent pipeline, and detached-task lifecycle are still being migrated onto those primitives.
## See also
-1
View File
@@ -180,5 +180,4 @@ Each connected account gets its own profile and its own IDB. CDP can snapshot on
## See also
* [`docs/TAURI_CEF_FINDINGS_AND_CHANGES.md`](../../docs/TAURI_CEF_FINDINGS_AND_CHANGES.md). the notification-permission deep dive.
* [`CLAUDE.md`](../../CLAUDE.md). the canonical "no new JS injection" rule.