[3/5] feat(orchestration): attention queue — aggregate approvals + blocked runs + unread into a 'Needs you' zone#4592
Conversation
Pure, cross-domain 'needs you' aggregation for the orchestration hub: kind ordering (approval → needs-input → unread, newest-first within a kind), tagged actions, counts, and the source-row → neutral-signal mappers. No I/O — fully unit-tested (10 cases) without a live approval gate, command center, or store. Kept out of the already-large ops.rs.
Renderer-only controller aggregating pending approvals + command-center NeedsInput runs + per-instance unread into one AttentionQueue. The handler is thin glue over the pure attention.rs mappers; the store read and command-center mapping are extracted into unit-tested helpers. Adds attention.rs to the log-leak-guard sources.
AttentionQueue/AttentionItem/AttentionAction types + orchestrationClient .attention() over the orchestration_attention RPC.
Presentational 'Needs you' zone: kind chip, title, one-line detail or unread count, and a kind-aware action button (Review approvals / Open runs + chats). Empty state renders a calm 'all caught up'. 7 tests.
Adds 8 attention-queue keys (title/empty/labels/actions) with real translations across en + 13 locales.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR adds an attention queue that aggregates approvals, needs-input runs, and unread sessions into one RPC-backed list. It includes new Rust queue assembly and controller wiring, a typed TypeScript client, React components and tests, and localized strings in 14 languages. ChangesAttention Queue Feature
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Schemas as schemas.rs
participant Approvals as approval_list_pending
participant Ops as ops.rs
participant Attention as attention.rs
Client->>Schemas: orchestration_attention RPC
Schemas->>Approvals: fetch pending approvals
Schemas->>Ops: collect needs-input and unread signals
Schemas->>Attention: assemble_attention(...)
Schemas-->>Client: AttentionQueue JSON
sequenceDiagram
participant AttentionQueueView
participant AttentionQueueItem
participant User
participant onAction
AttentionQueueView->>AttentionQueueItem: render item props
User->>AttentionQueueItem: click action button
AttentionQueueItem->>onAction: item.action
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b4a43a32f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| /// What the renderer should do when the user acts on an item. Tagged so the | ||
| /// frontend can `switch` on `type` and carry exactly the id it needs. | ||
| #[derive(Debug, Clone, PartialEq, Eq, Serialize)] | ||
| #[serde(tag = "type", rename_all = "kebab-case")] |
There was a problem hiding this comment.
Serialize attention action ids in camelCase
The enum-level rename_all only renames the variant tag, so the struct-variant fields still serialize as request_id, thread_id, run_id, and session_id. The new TS client declares requestId/threadId/runId/sessionId, so when orchestrationClient.attention() feeds an item action to the renderer router, the expected id fields are undefined and approval/open actions cannot target the right request, thread, run, or session.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 15a8aa1 — added rename_all_fields = "camelCase" so the action id fields serialize requestId/threadId/runId/sessionId, plus a serialization test asserting the tagged camelCase wire shape.
| /// unit-tested [`attention::needs_input_from_command_center`]. | ||
| fn command_center_needs_input(config: &Config) -> Vec<attention::NeedsInputSignal> { | ||
| use crate::openhuman::agent_orchestration::command_center::list_agent_work; | ||
| match list_agent_work(config, Some(ATTENTION_RUN_LIMIT)) { |
There was a problem hiding this comment.
Filter command-center reads before applying the limit
This calls list_agent_work with a global recent-run limit and then extracts the NeedsInput bucket, but list_agent_work queries with status: None and orders all runs by updated_at DESC LIMIT. In a busy workspace with more than 100 newer working/completed/failed runs, an older AwaitingUser run is outside the fetched page, so the attention queue silently omits a run that is still blocked on the user.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 5efdeb3 — command_center_needs_input now queries the ledger filtered to AwaitingUser runs (status filter applied before the limit) instead of paging global recent runs then filtering, so an older still-blocked run can't be paged out.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
app/src/components/intelligence/AttentionQueueItem.test.tsx (1)
9-35: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winTest fixtures assume camelCase action fields — flags the backend contract mismatch.
These fixtures (
requestId,threadId,sessionId) encode the expected wire shape. As flagged inattention.rs, the currentAttentionActionserde attributes will actually emit snake_case field names (request_id,thread_id,session_id), so these fixtures (and the real runtime payloads) will diverge once wired end-to-end. No change needed here once the backend fix (rename_all_fields) lands — noting for traceability.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/components/intelligence/AttentionQueueItem.test.tsx` around lines 9 - 35, The AttentionQueueItem fixtures already reflect the intended camelCase wire shape, so the mismatch is in the backend serialization contract. Update AttentionAction in the backend to use the appropriate serde field renaming so its emitted action fields match requestId/threadId/sessionId, and keep these test fixtures aligned with that contract by verifying the AttentionQueueItem cases continue to serialize against the same shape.
🧹 Nitpick comments (1)
src/openhuman/orchestration/schemas.rs (1)
592-609: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winUnread-bucket failure sinks the whole queue, unlike the command-center bucket.
command_center_needs_inputdegrades to an empty vec on error so approvals+unread still surface, but the approvals fetch (line 596-600) and the unread fetch viastore::with_connection(...).map_err(...)?(line 608-609) both propagate errors and fail the entirehandle_attentionrequest. A transient local-DB read hiccup on the unread bucket would then hide already-successfully-fetched approvals and needs-input signals too, defeating the stated goal of a resilient "needs you" queue.Consider applying the same best-effort/log-and-empty pattern to the unread bucket (and confirming approvals truly never errors, per the inline comment) for consistency.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/orchestration/schemas.rs` around lines 592 - 609, The attention queue should stay resilient when the unread bucket read fails, instead of aborting the whole request. Update the unread fetch in handle_attention so the store::with_connection(..., gather_unread_signals) path degrades to an empty vec or logged fallback on error, matching the best-effort behavior used by command_center_needs_input, and verify the approvals path around attention::approval_signals and approval_list_pending is truly non-fatal as the inline comment says.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/components/intelligence/AttentionQueueItem.tsx`:
- Around line 11-16: Update the return type annotation in AttentionQueueItem to
use a direct ReactElement type import from react instead of React.ReactElement,
since React is not imported in this module. Adjust the component’s type
signature accordingly and ensure the existing JSX/typing references in
AttentionQueueItem resolve without relying on a React namespace.
In `@src/openhuman/orchestration/schemas.rs`:
- Around line 584-662: The attention aggregation logic is living in the wrong
module: handle_attention and its helpers command_center_needs_input and
gather_unread_signals should not implement business logic in schemas.rs. Move
the orchestration work that combines approvals, command-center NeedsInput, and
unread signals into ops.rs as the RpcOutcome-style handler, keeping schemas.rs
as a thin RPC-facing wrapper that only parses params and delegates to that ops
function. Preserve the existing behavior and reuse the same unique symbols so
the wrapper still calls into the moved attention handler cleanly.
---
Duplicate comments:
In `@app/src/components/intelligence/AttentionQueueItem.test.tsx`:
- Around line 9-35: The AttentionQueueItem fixtures already reflect the intended
camelCase wire shape, so the mismatch is in the backend serialization contract.
Update AttentionAction in the backend to use the appropriate serde field
renaming so its emitted action fields match requestId/threadId/sessionId, and
keep these test fixtures aligned with that contract by verifying the
AttentionQueueItem cases continue to serialize against the same shape.
---
Nitpick comments:
In `@src/openhuman/orchestration/schemas.rs`:
- Around line 592-609: The attention queue should stay resilient when the unread
bucket read fails, instead of aborting the whole request. Update the unread
fetch in handle_attention so the store::with_connection(...,
gather_unread_signals) path degrades to an empty vec or logged fallback on
error, matching the best-effort behavior used by command_center_needs_input, and
verify the approvals path around attention::approval_signals and
approval_list_pending is truly non-fatal as the inline comment says.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 80363367-5cd5-4404-9d1d-a1cb6726214c
📒 Files selected for processing (23)
app/src/components/intelligence/AttentionQueue.test.tsxapp/src/components/intelligence/AttentionQueue.tsxapp/src/components/intelligence/AttentionQueueItem.test.tsxapp/src/components/intelligence/AttentionQueueItem.tsxapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/lib/orchestration/orchestrationClient.tssrc/openhuman/orchestration/attention.rssrc/openhuman/orchestration/mod.rssrc/openhuman/orchestration/ops.rssrc/openhuman/orchestration/schemas.rs
…attention-queue # Conflicts: # app/src/lib/orchestration/orchestrationClient.ts # src/openhuman/orchestration/schemas.rs
…humansai#4592) The enum-level rename_all only renames the variant tag, so struct-variant fields serialized snake_case (request_id, …) while the TS AttentionAction union declares requestId/threadId/runId/sessionId. Add rename_all_fields = "camelCase" so the wire matches, and lock it with a serialization test.
tinyhumansai#4592) React is not imported in this module, so React.ReactElement did not resolve cleanly. Use a named type import from react.
…cked-run query, harden unread (tinyhumansai#4592) - Relocate command_center_needs_input + gather_unread_signals (and the unread test) from schemas.rs into ops.rs, keeping schemas.rs a thin RPC wrapper per the canonical module shape. - Query the ledger filtered to AwaitingUser runs so the run limit bounds blocked runs only; the prior global-page-then-filter could page an older still-blocked run out in a busy workspace. - Make the unread-store read best-effort (log + empty) like the command-center read, so a transient local-DB hiccup can't sink already-resolved signals.
…4592) Seeds an AwaitingUser + a Running run and asserts command_center_needs_input surfaces only the blocked one — proving the status-filtered ledger query and keeping diff coverage on the rewritten helper.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/openhuman/orchestration/ops.rs (1)
969-988: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winN+1 query pattern with no upper bound in
gather_unread_signals.Unlike
command_center_needs_input, which is bounded byATTENTION_RUN_LIMIT, this loops over every session returned bylist_sessionsand issues a separateunread_countquery (itself akv_get+COUNT(*)) per session. For a workspace with many sessions, this is O(N) round trips perorchestration_attentioncall with no cap.Consider adding a single aggregate query in
store.rs(e.g., a join betweensessions/cursor KV andmessagesgrouped bysession_id) to replace the per-session loop, or at minimum bound the scan similarly toATTENTION_RUN_LIMIT.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/orchestration/ops.rs` around lines 969 - 988, The `gather_unread_signals` function is doing an unbounded N+1 scan by calling `store::unread_count` once per session after `store::list_sessions`, which can explode into many round trips. Replace the per-session loop with a single aggregate query in `store.rs` that computes unread counts grouped by `session_id` (joining the session/cursor data as needed) and then build `UnreadSignal` from that result. If a full aggregate isn’t practical, apply a cap similar to `ATTENTION_RUN_LIMIT` before iterating.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/openhuman/orchestration/ops.rs`:
- Around line 969-988: The `gather_unread_signals` function is doing an
unbounded N+1 scan by calling `store::unread_count` once per session after
`store::list_sessions`, which can explode into many round trips. Replace the
per-session loop with a single aggregate query in `store.rs` that computes
unread counts grouped by `session_id` (joining the session/cursor data as
needed) and then build `UnreadSignal` from that result. If a full aggregate
isn’t practical, apply a cap similar to `ATTENTION_RUN_LIMIT` before iterating.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 73291073-3521-4971-9a6a-e3685e9a1172
📒 Files selected for processing (19)
app/src/components/intelligence/AttentionQueueItem.tsxapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/lib/orchestration/orchestrationClient.tssrc/openhuman/orchestration/attention.rssrc/openhuman/orchestration/ops.rssrc/openhuman/orchestration/schemas.rs
✅ Files skipped from review due to trivial changes (6)
- app/src/lib/i18n/de.ts
- app/src/lib/i18n/pt.ts
- app/src/lib/i18n/en.ts
- app/src/lib/i18n/ar.ts
- app/src/lib/i18n/es.ts
- app/src/lib/i18n/ru.ts
🚧 Files skipped from review as they are similar to previous changes (11)
- app/src/lib/i18n/ko.ts
- app/src/lib/i18n/zh-CN.ts
- app/src/lib/i18n/it.ts
- app/src/components/intelligence/AttentionQueueItem.tsx
- app/src/lib/i18n/bn.ts
- app/src/lib/i18n/fr.ts
- app/src/lib/i18n/id.ts
- app/src/lib/i18n/hi.ts
- app/src/lib/i18n/pl.ts
- app/src/lib/orchestration/orchestrationClient.ts
- src/openhuman/orchestration/attention.rs
Summary
orchestration_attention— a renderer-only controller that folds the hub's three "needs you" signal sources into one priority-ordered queue: pending tool approvals, agent runs awaiting input (command-centerNeedsInput), and instances with unread messages.AttentionQueue("Needs you" zone) +AttentionQueueItem(kind chip · title · one-line detail / unread count · a single kind-aware action button).orchestration/attention.rsmodule (kind ordering, action mapping, counts) so the whole assembly is unit-tested without a live approval gate, command center, or store.Problem
A user running many agent instances has no single place that answers "what is blocked on me right now?". The signals exist but are scattered across three domains — the approval gate (
approval_list_pending), the command center'sNeedsInputbucket (runsawaiting_user), and per-sessionunread— with no aggregation and no shared shape the UI can render as one attention list.Solution
orchestration):orchestration_attentiongathers the three sources and hands them to a pureassemble_attentionthat emitsAttentionQueue { items: AttentionItem[], counts }. Items are ordered by kind urgency (approval → needs-input → unread), then newest-first within a kind. Each item carries a taggedaction(approval/open-thread/open-run/open-session) so the renderer knows where to route. Unread ships acount(not an English string) so the frontend localizes it. The command-center read is best-effort — a failure logs and yields an empty bucket rather than sinking the whole queue.AttentionQueue/AttentionItem/AttentionActiontypes +orchestrationClient.attention();AttentionQueue+AttentionQueueItemcomponents with co-located tests. An empty queue renders a calm "all caught up" state (stable anchor, not hidden).Submission Checklist
attention.rs) + the controller-count assertion; frontend: 7 cases across the two components (approval/needs-input/unread rendering, action dispatch, empty + loading states, no-handler safety).diff-coveragainstupstream/main: Rust 93% (attention.rs100%), frontend 90%. The pure aggregator + mappers and both components are directly unit-tested; the ~27 uncovered Rust lines are the async controller plumbing (config load / RPC awaits /to_json).## Related): none — no matrix IDs.Closes #NNN): design-driven, no tracking issue.Impact
action_summary; unread surfaces a count only; the log line emits counts, not content.Related
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
feat/orchestration-attention-queueValidation Run
pnpm --filter openhuman-app format:checkpnpm typecheckvitest run AttentionQueue / AttentionQueueItem(7 passed);cargo test --lib openhuman::orchestration::attention+::schemascargo fmt --check,cargo check,cargo clippy --libapp/src-taurichangesValidation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
Parity Contract
Duplicate / Superseded PR Handling
Summary by CodeRabbit