Skip to content

feat(chat): parallel agent inference (cross-thread + within-thread forks) - #3633

Merged
senamakel merged 7 commits into
tinyhumansai:mainfrom
senamakel:feat/parallism
Jun 12, 2026
Merged

senamakel merged 7 commits into
tinyhumansai:mainfrom
senamakel:feat/parallism

Conversation

@senamakel

Copy link
Copy Markdown
Member

Summary

  • Enable parallel agent inference — turns now run concurrently instead of one-at-a-time.
  • Cross-thread: send to thread B while thread A streams; the frontend's single global activeThreadId lock becomes a per-thread activeThreadIds set.
  • Within-thread: a new QueueMode::Parallel spawns an independent forked turn (snapshot-history-at-start, run alongside, commit append-only) instead of interrupting; Cmd/Ctrl+Enter sends it, rendered as an interleaved "branch" bubble.
  • Hardening: cooperative CancellationToken interruption (with timed hard-abort backstop) replaces the abrupt handle.abort().

Problem

The desktop app serialized agent inference: the Rust core was already structurally per-thread (each web turn is its own tokio::spawn, no lock held across the LLM call, conversation store is append-only under a global lock), but the frontend's single global activeThreadId flag blocked the composer whenever any thread had a turn in flight. There was also no way to run two turns on the same conversation, and interruption used a hard task abort that could tear a turn down mid-write.

Solution

  • Frontend (cross-thread): threadSlice.activeThreadId → activeThreadIds set; composer/voice/autocomplete gate on the selected thread only; the single-turn refs in Conversations.tsx (pending-send, 120s silence timer, status baseline) become per-thread maps keyed by reference-identity signatures.
  • Core (interruption): InFlightEntry carries a CancellationToken; the turn runs under tokio::select!; cancel_in_flight_gracefully() cancels cooperatively then hard-aborts after a 5s grace.
  • Core (within-thread): QueueMode::Parallel + run_chat_task(fork: bool) (a fork never touches the shared THREAD_SESSIONS agent cache) + a separate PARALLEL_IN_FLIGHT lane keyed by request_id; cancel_chat tears down the primary turn and all parallel turns.
  • Frontend (within-thread): additive parallelStreamsByThread lane (mirrors the core's separate lane rather than re-keying the whole slice, keeping the battle-tested primary stream path untouched); ChatRuntimeProvider routes forked-turn deltas/done/error to the lane; chatSend returns the request_id; handleSendParallel + a typeable-while-active composer render branch bubbles.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/pr-ci.yml. Run pnpm test:coverage and pnpm test:rust locally; PRs below 80% on changed lines will not merge.
  • Coverage matrix updated — added row 4.2.4 (Parallel inference) in docs/TEST-COVERAGE-MATRIX.md
  • All affected feature IDs from the matrix are listed in the PR description under ## Related
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: additive chat feature — no release-cut smoke surface changed (docs/RELEASE-MANUAL-SMOKE.md)
  • N/A: no tracking issue for this change

Impact

  • Desktop (Windows/macOS/Linux): users can run multiple conversations concurrently and fork a same-thread "branch" turn. Additive — default behaviour (interrupt-on-new-message) is unchanged.
  • No migration. No new network deps. Cooperative cancellation reduces the chance of a turn being killed mid-transcript-write; the canonical conversation store was already append-only under a global lock, so concurrent commits are safe.

Related

  • Coverage matrix feature IDs: 4.2.4 (Parallel inference — cross-thread + within-thread forked turns).
  • Closes:
  • Follow-up PR(s)/TODOs: dedicated WD/Playwright E2E spec for the parallel-branch flow; per-request tool-timeline/inference-status for forked turns (currently thread-keyed; only streaming text is per-request).

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/parallism
  • Commit SHA: d7820c6

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: full Vitest suite (5879 passed); Rust web-provider suite (80 passed, incl. 3 new concurrency/cancellation tests); slice + provider parallel-lane tests
  • Rust fmt/check (if changed): cargo fmt --check + pnpm rust:check (pre-push hook) green
  • N/A: Tauri fmt/check — no app/src-tauri Rust changed (verified by pre-push rust:check)

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: turns can run concurrently across threads, and an explicit parallel queue mode forks a concurrent same-thread turn.
  • User-visible effect: composer no longer globally locks while any thread is busy; Cmd/Ctrl+Enter sends a parallel branch rendered as its own bubble.

Parity Contract

  • Legacy behavior preserved: default queue mode stays interrupt; the primary streaming path and streamingAssistantByThread are untouched (additive parallel lane).
  • Guard/fallback/dispatch parity checks: QueueMode::Parallel added to all match arms; forked turns isolated from the THREAD_SESSIONS cache; narrow test stores defended via activeThreadIds ?? {}.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: N/A
  • Resolution: N/A

…llelism

Replace the global single `activeThreadId` lock with a per-thread
`activeThreadIds` set so turns on different threads run concurrently —
matching Codex's model where each conversation is the unit of concurrency
and the core already runs per-thread turns in isolated tokio tasks.

- threadSlice: `activeThreadId: string|null` -> `activeThreadIds: Record<string,true>`;
  add markThreadInferenceActive / clearThreadInferenceActive; setActiveThread
  kept as compat (string marks active, null clears all).
- Conversations: composer/voice/autocomplete gates now key on the *selected*
  thread only; convert single-turn refs (pending send, silence timer, status
  baseline) to per-thread maps/sets so concurrent sends each track their own
  state. Per-thread reference signatures keep the silence-timer rearm scoped
  to a thread's own activity.
- ChatRuntimeProvider: clear inference markers per thread on done/error;
  disconnect reconcile clears all active threads.
- socketService: re-subscribe to every active thread room on reconnect.
- Tests updated + new multi-active-thread coverage.

Phase 1 of parallel inference. Core hardening + within-thread fork model follow.
Replace the abrupt `handle.abort()` interrupt path with a cooperative
`CancellationToken` threaded into each turn's `tokio::select!`, with a
timed hard-abort backstop. Cancelling now tears the turn future down at a
safe await point — cancelling the in-flight LLM request and releasing locks
cleanly — instead of killing the task mid-write, which matters for the
append-only transcripts Phase 3 will rely on.

- InFlightEntry gains `cancel_token`; start_chat wraps the turn in a
  biased select on `token.cancelled()` with a quiet cooperative-cancel
  branch (the interrupting/cancelling side already emits chat_error).
- cancel_in_flight_gracefully(): cancel() then a detached 5s abort backstop.
  Used by both the interrupt-on-new-message path and cancel_chat.
- Tests: prove two distinct threads run turns concurrently, and that
  cancel_chat cooperatively drops the in-flight turn future (Drop guard
  fires well before the parked sleep would elapse). New test-only block hook
  with started/dropped signals.

Cross-thread parallelism was already safe in the core (per-thread keying,
no lock held across the LLM call); this hardens the interrupt path.
Phase 2 of parallel inference.
…thread

Adds within-thread parallel inference via the snapshot+append fork model.
A 'parallel' queue_mode spawns an independent turn that snapshots the
thread's history-at-start and runs ALONGSIDE the primary in-flight turn
instead of interrupting or queueing it.

- run_queue: new QueueMode::Parallel.
- run_chat_task gains a `fork` flag: a forked turn never reads or writes the
  shared THREAD_SESSIONS agent cache (always builds fresh from the history
  snapshot), so concurrent same-thread turns can't clobber each other's
  cached agent.
- ops: separate PARALLEL_IN_FLIGHT lane keyed by request_id; spawn_parallel_turn
  runs the fork with cooperative cancellation and emits the same per-request
  stream events as a primary turn (so the UI can render interleaved branches).
  cancel_chat now tears down the primary turn AND every parallel turn on the
  thread.
- Test: a parallel turn runs concurrently with the primary on the same thread
  (does not interrupt it), and a thread cancel tears down both.

Phase 3 (core) of parallel inference. Frontend trigger + interleaved bubble
rendering follow.
Exposes QueueMode::Parallel in the UI. A second turn on an already-active
thread can be sent as a concurrent forked 'branch' (Cmd/Ctrl+Enter), rendered
as its own interleaved bubble instead of clobbering the primary stream.

Mirrors the core's separate parallel lane rather than re-keying the whole
chatRuntime slice (which would destabilise the primary streaming path):
- chatRuntimeSlice: additive parallelStreamsByThread (threadId->requestId->stream)
  + parallelRequestThreads map; registerParallelRequest / setParallelStream /
  clearParallelRequest; wired into clearRuntimeForThread / clearAllChatRuntime.
- ChatRuntimeProvider: onTextDelta/onThinkingDelta route forked-turn deltas to
  the parallel lane; onDone/onError resolve only the forked lane, leaving the
  primary turn's stream/status/lifecycle/active marker intact.
- chatService: QueueMode gains 'parallel'; chatSend returns the request_id.
- Conversations: handleSendParallel (own path, never touches the primary
  lifecycle) + Cmd/Ctrl+Enter trigger; renders parallel branch bubbles.
- ChatComposer: allowParallelSend keeps the textarea editable while the thread
  is active; branch hint placeholder.
- i18n: chat.parallelBranchHint / chat.parallelBranchLabel in all 14 locales.
- Tests: parallel-lane reducers + provider routing; reset session usage in the
  provider test harness for isolation.

Phase 3c of parallel inference — feature complete (cross-thread + within-thread).
# Conflicts:
#	app/src/components/chat/ChatComposer.tsx
@senamakel
senamakel requested a review from a team June 12, 2026 16:18
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@senamakel, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 3 hours, 34 minutes, and 31 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97f0c4f5-f0de-461b-b820-a447c114a964

📥 Commits

Reviewing files that changed from the base of the PR and between a557fc6 and d7820c6.

📒 Files selected for processing (36)
  • app/src/components/chat/ChatComposer.tsx
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.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
  • app/src/pages/Conversations.tsx
  • app/src/pages/__tests__/Conversations.attachments.test.tsx
  • app/src/pages/__tests__/Conversations.render.test.tsx
  • app/src/pages/__tests__/Conversations.test.tsx
  • app/src/pages/__tests__/Conversations.welcomeLock.test.tsx
  • app/src/providers/ChatRuntimeProvider.tsx
  • app/src/providers/__tests__/ChatRuntimeProvider.test.tsx
  • app/src/services/chatService.ts
  • app/src/services/socketService.ts
  • app/src/store/__tests__/chatRuntimeSlice.test.ts
  • app/src/store/__tests__/threadSlice.test.ts
  • app/src/store/chatRuntimeSlice.ts
  • app/src/store/threadSlice.ts
  • docs/TEST-COVERAGE-MATRIX.md
  • src/openhuman/agent/harness/run_queue/mod.rs
  • src/openhuman/agent/harness/run_queue/types.rs
  • src/openhuman/channels/providers/web/mod.rs
  • src/openhuman/channels/providers/web/ops.rs
  • src/openhuman/channels/providers/web/run_task.rs
  • src/openhuman/channels/providers/web/types.rs
  • src/openhuman/channels/providers/web_tests.rs

Comment @coderabbitai help to get the list of available commands and usage tips.

@senamakel
senamakel merged commit 9af5994 into tinyhumansai:main Jun 12, 2026
18 of 19 checks passed
senamakel added a commit to senamakel/openhuman that referenced this pull request Jun 12, 2026
After the upstream tinyhumansai#3633 (parallel inference) merge, run_chat_task's future is
very large. Adding the per-profile with_source_scope wrapper inlined the giant
run_single future into an extra ambient-scope layer, pushing the run_chat_task
frame past the default test-thread stack — every web-channel turn coverage test
aborted with 'stack overflow' under cargo-llvm-cov (and plain cargo test).

Box the run_single future so both ambient-scope wrappers (thread_id + source
scope) hold a pointer instead of inlining it. No behaviour change. Verified the
previously-aborting lib + channels_web_* e2e targets now pass.
senamakel added a commit to nocstah/openhuman that referenced this pull request Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant