Skip to content

feat(groups): I6 — humans as group members (AWAITING_HUMAN_INPUT turns) - #640

Merged
ginccc merged 4 commits into
mainfrom
feat/group-i6-human-members
Aug 8, 2026
Merged

feat(groups): I6 — humans as group members (AWAITING_HUMAN_INPUT turns)#640
ginccc merged 4 commits into
mainfrom
feat/group-i6-human-members

Conversation

@ginccc

@ginccc ginccc commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

Implements I6 — Human as group member from planning/group-collaboration-improvements-plan.md (queue position 4 in planning/group-collaboration-NEXT.md, after I17 #637, I14 #638, I8 #639). Real deployments are hybrid teams: a memberType: "HUMAN" member sits in the roster like any agent, but their turn pauses the discussion in a new state AWAITING_HUMAN_INPUT until they answer — deliberately NOT AWAITING_APPROVAL (approval endpoints must never accept free text; inboxes must tell "approve/reject" from "you're up").

Design (per the plan, decisions honored)

Turn flow

The phase loops intercept HUMAN speakers before any LLM machinery, render their input exactly like an agent's (buildPhaseInput), and surface a HumanTurnRequired control-flow signal; executeDiscussion catches it and GroupHitlCoordinator.commitHumanTurnPause persists:

  • PendingHumanInput{memberId, displayName, phaseIdx, repeatIdx, speakerIdx, entryType, renderedPrompt, onTimeout, requestedAt} — the prompt survives restarts and the UI reads it from the conversation;
  • the F2 ResumePoint — writer-less until now; this is its first producer;
  • the turn is counted at the pause (pausedTurnCount = turns + 1) so a human turn is never free.

Submission

POST /groups/{groupId}/conversations/{id}/human-input {memberId, content} + MCP submit_group_human_input. Authorization is a new, deliberately narrower guard (HitlAccessGuard.requireGroupHumanInputAccess): the pending member's own principal or an admin — an eddi-approver may decide approvals they don't own, but speaking as another human is impersonation, not review. The answer is recorded as the phase's natural entry type (captured at pause time so a config edit can't re-type it), the bookmark advances past the answered speaker (speakerIdx + 1, per the plan), the CAS out of AWAITING_HUMAN_INPUT turns double-submits into 409s, and the discussion re-enters on a background thread exactly like an approval resume. Drift checks run before any mutation — a stale bookmark refuses the submission instead of needing a rollback; the one post-CAS failure (executor saturation) rolls the append back and restores the pause.

Timeouts

humanMemberConfig {turnTimeout (ISO-8601, null = wait indefinitely), onTimeout = SKIP_TURN | ABORT} reuses the HITL schedule machinery on a new surface group-human — SKIP_TURN/ABORT are not HitlTimeoutPolicy values, so HitlTimeoutHandler branches on the surface before parsing. SKIP_TURN writes the plan's SKIPPED entry ("No response from within ") and resumes; ABORT cancels gracefully. Crash recovery re-arms human-turn timeouts (policy bookmarked on the pending record, so a config edit while paused cannot change what the pause promised).

PARALLEL phases

Humans never join the fan-out (an LLM answers in seconds; a paused future would pin the batch). Agents run first, then humans are prompted sequentially against the pre-fan-out snapshot — an independent round stays independent. The one carve-out from "PARALLEL never honors a bookmark": a HUMAN_TURN_PARALLEL resume skips the fan-out and resumes the human tail, so agent turns are not duplicated on resume.

Save-time matrix (AgentGroupStore.validateHumanMembers, hard-throws)

Hard rejection is safe where the older checks had to warn: no legacy document can contain the new enum value. displayName required; humans rejected in task-force groups (PLAN/EXECUTE/VERIFY — wave workers can't pause) and targetEachPeer phases (a human on both axes owes up to 2(N−1) pauses and the flat bookmark has no (speaker,target) coordinate) — preset-expanded, or the check is inert for preset-style groups; nested groups containing humans rejected one level deep, with a runtime backstop (MemberTurnExecutor cancels a stranded AWAITING_HUMAN_INPUT child, like nested HITL). turnTimeout must parse. A human moderator is allowed + warned — and resolveParticipants now preserves the roster member's type for the moderator id (the 4-arg ctor silently demoted a human moderator to an agent).

Surfaces

human_input_requested event (constant → record → listener default → SSE forward + OpenAPI list → Slack "you're up" notice, mrkdwn-escaped); pending human turns join the existing inbox as pauseType: "HUMAN_TURN" + pendingMemberId (no third inbox) and the member sees their own turns without owning the conversation; availableActions gains submitHumanInput; MCP get_group_approval_status reports the pending member + rendered prompt; every cancel path treats the new state as a first-class pause. Defense in depth: a HUMAN reaching executeAgentTurn (judge/dissent/task-wave/nested — contexts that cannot pause) yields SKIPPED.

Deviation, recorded

The I14 HUMAN_DECIDES tie-policy stays save-time-rejected: I14 (#638) is not merged, so the wiring is a small follow-up once both branches land (I12 needs both anyway).

Tests (+23 across 8 classes; 1869 tests green across engine.internal + configs.groups + engine.hitl; checkstyle clean)

Sequential pause (rendered prompt, absolute index, budget-before-human ordering); parallel fan-out-then-human with pre-fan-out blindness asserted via captor, resume-tail without fan-out re-run, post-resume blindness; pause-commit shape (bookmark/pending/schedule surface + policy); submit→record→advance→CAS→re-enter (captured runnable proves the re-entry coordinates); wrong member / wrong state / blank / oversize / config drift all refuse before mutation; SKIP_TURN timeout writes the named SKIPPED entry and advances; cancel-of-human-pause clears the pending turn; timeout-handler routing (SKIP_TURN / ABORT / unknown-degrades-to-skip); guard matrix (member ✓, admin ✓, owner+approver ✗ FORBIDDEN, wrong-group 404, auth-off no-op, inbox shows members their own turns); save-time matrix; human-moderator preservation; defense-in-depth skip; 4 enum pins updated (the I14/I8 CI lesson — caught locally this time).

Summary by CodeRabbit

  • New Features
    • Added support for human members in group conversations.
    • Discussions pause for human input and resume after authenticated responses.
    • Added HTTP and MCP submission options, status details, live events, and Slack notifications.
    • Supports sequential and parallel discussions while preserving progress.
  • Bug Fixes
    • Added timeout handling to skip a turn or abort a discussion.
    • Improved access controls, cancellation, recovery, and validation.
  • Documentation
    • Documented configuration, input submission, timeout behavior, and restrictions.

A MemberType.HUMAN member sits in the roster like any agent, but their
turn pauses the discussion in a new AWAITING_HUMAN_INPUT state until
they submit - deliberately not AWAITING_APPROVAL: approval endpoints
never accept free text, and an inbox must tell "approve/reject" from
"you are up".

- Phase loops intercept HUMAN speakers, render their input exactly like
  an agent turn, and commit a pause carrying PendingHumanInput + the F2
  ResumePoint (its first producer). The turn is counted at the pause.
- Submission via POST .../human-input + MCP submit_group_human_input;
  only the pending member''s own principal (or admin) may submit - an
  approver may decide approvals, but speaking as another human is
  impersonation. Entry lands as the phase''s natural type; bookmark
  advances; CAS makes double-submits a 409; drift-checks refuse before
  any mutation.
- humanMemberConfig {turnTimeout, onTimeout=SKIP_TURN|ABORT} rides the
  HITL schedule machinery on a new group-human surface; crash recovery
  re-arms human-turn timeouts; SKIP_TURN writes a named SKIPPED entry.
- PARALLEL: agents fan out first, humans prompted sequentially against
  the pre-fan-out snapshot; a HUMAN_TURN_PARALLEL resume skips the
  fan-out instead of duplicating agent turns.
- Save-time matrix: displayName required; no humans in task-force or
  targetEachPeer groups; nested groups with humans rejected (+ runtime
  backstop); human moderator allowed + warned, and resolveParticipants
  no longer demotes them to an agent.
- human_input_requested event (SSE/Slack), HUMAN_TURN inbox entries
  with pendingMemberId, availableActions submitHumanInput.
@ginccc
ginccc requested a review from rolandpickl as a code owner August 8, 2026 01:04
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ginccc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 24 seconds

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5bfbfd3c-db19-4c0d-a4a3-c253f14f72ad

📥 Commits

Reviewing files that changed from the base of the PR and between ab5c115 and 9862478.

📒 Files selected for processing (7)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/configs/groups/model/GroupConversation.java
  • src/main/java/ai/labs/eddi/engine/internal/RestGroupConversation.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpHitlTools.java
  • src/test/java/ai/labs/eddi/engine/internal/GroupConversationServiceExtendedTest.java
  • src/test/java/ai/labs/eddi/engine/internal/RestGroupConversationHitlTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpHitlToolsTest.java
📝 Walkthrough

Walkthrough

The PR adds HUMAN members to group conversations. Human turns pause execution in AWAITING_HUMAN_INPUT, persist resume metadata, support timeout handling, and resume through REST or MCP submissions. SSE, inbox, Slack, cancellation, recovery, validation, and tests are updated.

Changes

Human group-member HITL

Layer / File(s) Summary
Human-member contracts and validation
src/main/java/ai/labs/eddi/configs/groups/model/..., src/main/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStore.java, src/test/java/ai/labs/eddi/configs/groups/...
Adds HUMAN members, timeout policies, pending-input state, and save-time configuration validation.
Sequential and parallel human turns
src/main/java/ai/labs/eddi/engine/internal/groups/..., src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java, src/test/java/ai/labs/eddi/engine/internal/groups/...
Pauses before human turns, preserves resumable indexes, separates parallel agent fan-out from human turns, and skips unsupported automated human execution.
Pause persistence, timeout, and resumption
src/main/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinator.java, src/main/java/ai/labs/eddi/engine/internal/HitlTimeoutHandler.java, src/main/java/ai/labs/eddi/engine/hitl/..., src/test/java/ai/labs/eddi/engine/internal/...
Persists pauses, schedules SKIP_TURN or ABORT, validates and records resolutions, resumes discussions, audits outcomes, and handles cancellation and recovery.
Authorized submission and notification surfaces
src/main/java/ai/labs/eddi/engine/api/..., src/main/java/ai/labs/eddi/engine/internal/RestGroupConversation.java, src/main/java/ai/labs/eddi/engine/mcp/..., src/main/java/ai/labs/eddi/integrations/slack/..., docs/...
Adds REST and MCP submission paths, access checks, SSE events, inbox summaries, Slack notifications, and documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GroupMember
  participant REST_or_MCP
  participant HitlAccessGuard
  participant GroupHitlCoordinator
  participant GroupConversationService
  GroupMember->>REST_or_MCP: submit human input
  REST_or_MCP->>HitlAccessGuard: verify pending-member access
  REST_or_MCP->>GroupHitlCoordinator: submitHumanInput
  GroupHitlCoordinator->>GroupConversationService: resume discussion
Loading

Possibly related PRs

  • labsai/EDDI#585: Both PRs modify group-conversation HITL pause and resume handling.
  • labsai/EDDI#626: Both PRs modify the group execution and lifecycle components used by human-turn handling.
  • labsai/EDDI#420: Both PRs modify group-conversation phase execution and turn limits.

Suggested reviewers: rolandpickl

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding human members to groups with the AWAITING_HUMAN_INPUT state.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/group-i6-human-members

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (7)
src/test/java/ai/labs/eddi/configs/groups/model/GroupConversationTest.java (1)

297-302: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the new availableActions branch.

The enum assertion is correct. The AvailableActionsTests nest in this file has no case for AWAITING_HUMAN_INPUT, and the nonTerminal test at lines 392-393 deliberately omits that state. Nothing pins the new "submitHumanInput" value, which is serialized to REST and MCP clients.

💚 Proposed test
`@Test`
`@DisplayName`("AWAITING_HUMAN_INPUT offers submitHumanInput")
void awaitingHumanInput() {
    var gc = new GroupConversation();
    gc.setState(GroupConversationState.AWAITING_HUMAN_INPUT);
    assertEquals(List.of("submitHumanInput"), gc.getAvailableActions());
}
🤖 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/test/java/ai/labs/eddi/configs/groups/model/GroupConversationTest.java`
around lines 297 - 302, Add a test in the AvailableActionsTests nest covering
GroupConversationState.AWAITING_HUMAN_INPUT, setting that state on a
GroupConversation and asserting getAvailableActions() returns exactly
["submitHumanInput"].
src/main/java/ai/labs/eddi/configs/groups/model/GroupConversation.java (1)

825-827: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the action name with the existing casing convention.

The existing action strings are lowercase: "followup", "continue", "close". The new value is camelCase: "submitHumanInput". availableActions is serialized to REST and MCP clients, so a client that maps action strings to UI controls now handles two casing styles. Pick one convention before the value ships.

♻️ Proposed rename
-            case AWAITING_HUMAN_INPUT -> List.of("submitHumanInput");
+            case AWAITING_HUMAN_INPUT -> List.of("submit-human-input");
🤖 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/main/java/ai/labs/eddi/configs/groups/model/GroupConversation.java`
around lines 825 - 827, Update the AWAITING_HUMAN_INPUT branch in
GroupConversation’s availableActions mapping to use the established lowercase
action-name convention instead of "submitHumanInput". Keep the action’s meaning
unchanged and ensure the serialized REST and MCP value matches the existing
lowercase actions.
src/test/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStoreTest.java (1)

160-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for a null member list.

Both helpers always supply a non-null member list: config(...) leaves the field at its default empty list, and humanConfig(...) passes List.of(members). No test reaches the config.getMembers() == null branch of humanMemberProblems, which is the same input shape that NPEs in validateHumanMembers.

🤖 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/test/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStoreTest.java`
around lines 160 - 166, Extend agentOnlyGroups_produceNoHumanProblems to cover
configurations whose members field is null, asserting humanMemberProblems
returns empty and hasHumanMembers remains false without throwing. Construct the
test input so config.getMembers() is explicitly null, exercising the same
null-member path used by validateHumanMembers.
src/main/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutor.java (1)

167-177: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider counting the human skip like the member-pause skip.

The comment states this branch mirrors handleMemberPause's SKIP precedent, but it omits both of that method's observability signals: counterGroupMemberPauseSkipped.increment() and listener.onMemberPauseSkipped(...). A human reached by an automated sub-round (convergence judge, dissent round, task-force wave) therefore produces a transcript entry with no metric and no event, so operators cannot see how often a configuration routes automated turns at people.

The listener argument is already in scope on this overload.

🤖 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/main/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutor.java`
around lines 167 - 177, Update the HUMAN-member branch in MemberTurnExecutor to
mirror handleMemberPause observability before returning the SKIPPED
TranscriptEntry: increment counterGroupMemberPauseSkipped and invoke
listener.onMemberPauseSkipped(...) using the available listener and relevant
member/turn context.
src/test/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinatorTest.java (1)

382-412: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the executor-saturation rollback.

This class exercises the happy path and every pre-mutation refusal, but not the one post-CAS failure branch. Make executorService.submit(...) throw a RejectedExecutionException and assert that resolveHumanTurn removes the appended entry, restores AWAITING_HUMAN_INPUT with the pending record and the original bookmark, re-arms the schedule, and throws GroupDiscussionException. That branch mutates the most state and currently has no regression guard.

🤖 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/test/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinatorTest.java`
around lines 382 - 412, Extend GroupHitlCoordinatorTest with a resolveHumanTurn
saturation test that makes executorService.submit(...) throw
RejectedExecutionException after the human entry is appended. Assert that the
operation throws GroupDiscussionException and rolls back the transcript entry,
restores AWAITING_HUMAN_INPUT with the original pending-human record and resume
bookmark, and re-arms the schedule using the existing store/service verification
patterns.
src/test/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngineTest.java (1)

240-273: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a resume case for a phase with repeats > 1.

Both resume tests use phase(TurnOrder.PARALLEL), whose helper fixes repeats = 1, so every pause and resume in this class carries repeatIdx = 0. That is the one repeat index the phase loop in GroupConversationService.executeDiscussion can match, because the loop always restarts at repeat = 0. A test that pauses at repeatIdx = 1 would cover the resume path a ROUND_TABLE discussion phase actually takes.

🤖 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/test/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngineTest.java`
around lines 240 - 273, Extend the parallel-phase resume coverage with a case
using a phase configuration whose repeats value is greater than one, and set the
invocation’s repeat index to 1. Preserve the existing assertions for resuming
without rerunning fan-out and for the next-human pause, ensuring the test
exercises the repeat-aware resume path used by
GroupConversationService.executeDiscussion.
src/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java (1)

644-650: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The zero-remaining-turns early return also skips the human tail.

When maxTurns <= 0, remainingTurns is derived from agentSpeakers.size(). A phase whose speakers are all HUMAN then has remainingTurns == 0 and returns at Line 646, so promptHumanTail never runs and no human is prompted — even though maxTurns <= 0 means "no turn cap".

executeDiscussion always passes maxTurns > 0 (it defaults to 50), so this is currently only reachable through direct callers and tests. Deriving the uncapped size from speakers.size() keeps the branch honest.

♻️ Proposed adjustment
-        int remainingTurns = maxTurns > 0 ? Math.max(0, maxTurns - turnCounter.get()) : agentSpeakers.size();
-        if (remainingTurns == 0) {
+        int remainingTurns = maxTurns > 0 ? Math.max(0, maxTurns - turnCounter.get()) : speakers.size();
+        if (remainingTurns == 0) {
+            // An exhausted turn budget owes no more turns — the human's included.
             return;
         }
         List<GroupMember> batchSpeakers = maxTurns > 0
                 ? agentSpeakers.subList(0, Math.min(agentSpeakers.size(), remainingTurns))
                 : agentSpeakers;
🤖 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/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java`
around lines 644 - 650, Update the remaining-turn calculation in
executeDiscussion so the uncapped branch uses the full speakers collection, not
agentSpeakers.size(). Preserve the zero-turn early return for capped discussions
while allowing maxTurns <= 0 phases containing only HUMAN speakers to continue
through promptHumanTail.
🤖 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 `@src/main/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStore.java`:
- Around line 90-114: Guard validateHumanMembers in AgentGroupStore.java (lines
90-114) with an early return when config.getMembers() is null, before the
nested-group loop and moderator stream. In AgentGroupStoreTest.java (lines
160-166), add coverage using setMembers(null) and assert humanMemberProblems
returns an empty list and hasHumanMembers returns false.
- Around line 157-165: Update the humanMemberConfig.turnTimeout validation in
AgentGroupStore to retain the parsed Duration, then reject values that are zero
or negative in addition to malformed ISO-8601 values. Add the existing
diagnostic to problems for any non-positive duration, avoiding a separate
pure-method call on the parse result.

In `@src/main/java/ai/labs/eddi/engine/api/IGroupConversationService.java`:
- Around line 161-198: Update GroupLifecycleOps.deleteGroupConversation to
include AWAITING_HUMAN_INPUT alongside AWAITING_APPROVAL in the paused-deletion
cleanup branch, ensuring the timeout schedule is canceled and deferred terminal
cleanup proceeds when a human turn is pending.

In `@src/main/java/ai/labs/eddi/engine/hitl/HitlAccessGuard.java`:
- Around line 159-160: In HitlAccessGuard, extend authorization for the current
pendingHumanInput.memberId so that only that member receives a minimal
pending-human-input view containing renderedPrompt, without exposing the full
transcript; preserve existing owner/admin/approver access. In
docs/group-conversations.md lines 181-193, document that the UI reads
pendingHumanInput.renderedPrompt only after the member-scoped prompt view is
available.

In `@src/main/java/ai/labs/eddi/engine/hitl/HitlCrashRecoveryObserver.java`:
- Around line 380-386: Update the repair flow containing the AWAITING_APPROVAL
scan and repairGroupHumanPaused() so each scan has independent exception
handling. Ensure an exception from the AWAITING_APPROVAL query or loop is logged
and does not prevent repairGroupHumanPaused() from running, while preserving the
existing return/count behavior for successful repairs.

In `@src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java`:
- Around line 1023-1027: Update the finally-block condition that guards
signingGuard.forgetConversation in GroupConversationService so it preserves the
signing cursor for both AWAITING_APPROVAL and AWAITING_HUMAN_INPUT; only forget
the conversation for other states.
- Around line 721-735: Preserve the resumePoint.repeatIdx() before clearing the
bookmark and use it to initialize the repeat loop in executeDiscussion, rather
than always starting at repeat 0. Apply the saved repeat offset only for the
matching phase and retain the existing speaker and parallel-human offset
handling, so resumed repeats skip all earlier agents and humans without changing
fresh executions.

In `@src/main/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinator.java`:
- Around line 1160-1163: Update resolveHumanTurn so
counterGroupHitlResume.increment() and auditHumanTurnResolution(...) occur only
after executorService.submit(...) succeeds, preserving rollback behavior when
submission is rejected. In the rollback path, replace the direct
activeTokens.remove(gc.getId()) with removeTokenAndConvertIfSignalled(gc,
listener) so cancellations signalled during restoration are preserved.
- Around line 1209-1210: Update the return path in the surrounding
GroupHitlCoordinator method to re-read and return a fresh group-conversation
copy instead of the live gc instance. Preserve the existing persistence flow,
and ensure RestGroupConversation.submitHumanInput receives the freshly-read
object so resumeWork mutations cannot race response serialization.

In `@src/main/java/ai/labs/eddi/engine/internal/groups/GroupLifecycleOps.java`:
- Around line 496-503: Update the pending retrieval flow in GroupLifecycleOps so
AWAITING_HUMAN_INPUT candidates are not excluded when AWAITING_APPROVAL rows
consume the limit. Fetch and merge sufficient candidates from both states before
HitlAccessGuard filtering, preferably through a state-set query scoped by group
and pending member, then apply the visible-result limit after filtering.

In `@src/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java`:
- Around line 612-633: Update the resume validation in resumeDiscussion for
RESUME_KIND_HUMAN parallel bookmarks to compare speakerIdx against the resolved
humans sublist size, not the full speaker roster. Reject or route through
existing drift handling when the index is outside that human-only range,
preventing executeParallelPhase from passing an invalid humanResumeIdx to
promptHumanTail.

In `@src/main/java/ai/labs/eddi/engine/internal/RestGroupConversation.java`:
- Around line 887-893: Update onHumanInputRequested in the listener to count
down completionLatch after sending the human-input event and closing the stream,
mirroring the finally behavior in onHitlPause so awaitCompletion is released for
this terminal listener.

---

Nitpick comments:
In `@src/main/java/ai/labs/eddi/configs/groups/model/GroupConversation.java`:
- Around line 825-827: Update the AWAITING_HUMAN_INPUT branch in
GroupConversation’s availableActions mapping to use the established lowercase
action-name convention instead of "submitHumanInput". Keep the action’s meaning
unchanged and ensure the serialized REST and MCP value matches the existing
lowercase actions.

In `@src/main/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutor.java`:
- Around line 167-177: Update the HUMAN-member branch in MemberTurnExecutor to
mirror handleMemberPause observability before returning the SKIPPED
TranscriptEntry: increment counterGroupMemberPauseSkipped and invoke
listener.onMemberPauseSkipped(...) using the available listener and relevant
member/turn context.

In `@src/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java`:
- Around line 644-650: Update the remaining-turn calculation in
executeDiscussion so the uncapped branch uses the full speakers collection, not
agentSpeakers.size(). Preserve the zero-turn early return for capped discussions
while allowing maxTurns <= 0 phases containing only HUMAN speakers to continue
through promptHumanTail.

In `@src/test/java/ai/labs/eddi/configs/groups/model/GroupConversationTest.java`:
- Around line 297-302: Add a test in the AvailableActionsTests nest covering
GroupConversationState.AWAITING_HUMAN_INPUT, setting that state on a
GroupConversation and asserting getAvailableActions() returns exactly
["submitHumanInput"].

In `@src/test/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStoreTest.java`:
- Around line 160-166: Extend agentOnlyGroups_produceNoHumanProblems to cover
configurations whose members field is null, asserting humanMemberProblems
returns empty and hasHumanMembers remains false without throwing. Construct the
test input so config.getMembers() is explicitly null, exercising the same
null-member path used by validateHumanMembers.

In
`@src/test/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinatorTest.java`:
- Around line 382-412: Extend GroupHitlCoordinatorTest with a resolveHumanTurn
saturation test that makes executorService.submit(...) throw
RejectedExecutionException after the human entry is appended. Assert that the
operation throws GroupDiscussionException and rolls back the transcript entry,
restores AWAITING_HUMAN_INPUT with the original pending-human record and resume
bookmark, and re-arms the schedule using the existing store/service verification
patterns.

In
`@src/test/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngineTest.java`:
- Around line 240-273: Extend the parallel-phase resume coverage with a case
using a phase configuration whose repeats value is greater than one, and set the
invocation’s repeat index to 1. Preserve the existing assertions for resuming
without rerunning fan-out and for the next-human pause, ensuring the test
exercises the repeat-aware resume path used by
GroupConversationService.executeDiscussion.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 360a560f-db68-4ed9-ac9f-93a99086c18c

📥 Commits

Reviewing files that changed from the base of the PR and between 22852f0 and 9c84783.

📒 Files selected for processing (31)
  • docs/changelog.md
  • docs/group-conversations.md
  • src/main/java/ai/labs/eddi/configs/groups/model/AgentGroupConfiguration.java
  • src/main/java/ai/labs/eddi/configs/groups/model/GroupConversation.java
  • src/main/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStore.java
  • src/main/java/ai/labs/eddi/engine/api/IGroupConversationService.java
  • src/main/java/ai/labs/eddi/engine/api/IRestGroupConversation.java
  • src/main/java/ai/labs/eddi/engine/hitl/HitlAccessGuard.java
  • src/main/java/ai/labs/eddi/engine/hitl/HitlCrashRecoveryObserver.java
  • src/main/java/ai/labs/eddi/engine/hitl/HitlSchedules.java
  • src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/HitlTimeoutHandler.java
  • src/main/java/ai/labs/eddi/engine/internal/RestGroupConversation.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinator.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/GroupLifecycleOps.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutor.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java
  • src/main/java/ai/labs/eddi/engine/lifecycle/GroupConversationEventSink.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpHitlTools.java
  • src/main/java/ai/labs/eddi/engine/model/PendingApprovalSummary.java
  • src/main/java/ai/labs/eddi/integrations/slack/SlackGroupDiscussionListener.java
  • src/test/java/ai/labs/eddi/configs/groups/model/AgentGroupConfigurationTest.java
  • src/test/java/ai/labs/eddi/configs/groups/model/GroupConversationHitlTest.java
  • src/test/java/ai/labs/eddi/configs/groups/model/GroupConversationTest.java
  • src/test/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStoreTest.java
  • src/test/java/ai/labs/eddi/engine/hitl/HitlAccessGuardTest.java
  • src/test/java/ai/labs/eddi/engine/internal/GroupConversationServiceTest.java
  • src/test/java/ai/labs/eddi/engine/internal/HitlTimeoutHandlerTest.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinatorTest.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutorTest.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngineTest.java

Comment thread src/main/java/ai/labs/eddi/engine/hitl/HitlAccessGuard.java
Comment thread src/main/java/ai/labs/eddi/engine/hitl/HitlCrashRecoveryObserver.java Outdated
Comment thread src/main/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinator.java Outdated
Comment thread src/main/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinator.java Outdated
Comment thread src/main/java/ai/labs/eddi/engine/internal/groups/GroupLifecycleOps.java Outdated
- McpToolFilter whitelists submit_group_human_input (CI guard test).
- New requireGroupConversationReadAccess: the pending human member may
  read the status carrying their rendered prompt (REST + MCP, summary
  now includes pendingMemberId/prompt on both surfaces); the full
  transcript view stays role-gated.
- Mid-phase resume starts at the bookmarked repeat instead of
  replaying earlier repeats (duplicate turns and spend).
- Human-turn resolution: metric/audit/resume-event deferred until the
  executor submit succeeds; rollback re-checks the control token; the
  caller gets a freshly-read copy, not the live instance the
  background leg mutates.
- Slack listener releases its completion latch on a human pause;
  deleting an AWAITING_HUMAN_INPUT conversation runs the paused
  cleanup; the signing cursor survives a human pause; crash-recovery
  sweeps isolated; inbox merges both pause states oldest-first.
- turnTimeout must be positive; null members list cannot NPE; F2
  drift guard scoped to approval bookmarks; CodeQL logs sanitized;
  HumanTurnRequired @PARAM docs moved to the constructor.
…pause

A human turn pauses mid-repeat, after other speakers appended this
repeat's entries; the resumed leg recomputed the slice base from the
current transcript size, so the repeat slice covered only post-pause
entries - the convergence check (and, once merged with I14, the VOTE
tally) silently lost every pre-pause contribution. New persisted
pausedRepeatSliceBase, written at the pause commit and consumed
exactly once with the speaker-bookmark read-and-clear discipline.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/ai/labs/eddi/engine/hitl/HitlAccessGuard.java (1)

159-160: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Apply the caller filter before the result limit.

listGroupPendingApprovals(groupId, limit) receives the limit before this filter runs. If other users fill that window, the pending member can receive an empty inbox even when their assigned turn exists.

Add a scoped service or store query for ownerId OR pendingMemberId, then apply limit after that predicate.

🤖 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/main/java/ai/labs/eddi/engine/hitl/HitlAccessGuard.java` around lines 159
- 160, Update listGroupPendingApprovals so the callerId predicate is applied in
the scoped service/store query before limiting results; add or reuse a query
filtering ownerId or pendingMemberId, then apply limit to the filtered results
so the assigned pending member’s approvals are included.
🤖 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 `@src/main/java/ai/labs/eddi/configs/groups/model/GroupConversation.java`:
- Around line 165-174: Increment CURRENT_SCHEMA_VERSION from 3 to 4 and register
the corresponding v3-to-v4 migration for GroupConversation documents. Ensure the
migration preserves legacy behavior by initializing pausedRepeatSliceBase to -1,
while retaining the existing newer-document refusal guard.

In `@src/main/java/ai/labs/eddi/engine/internal/RestGroupConversation.java`:
- Around line 730-740: Introduce a separate awaitingApproval predicate in the
full-detail authorization logic of RestGroupConversation, and use it instead of
paused for the approver branch so approvers can read transcripts only during
AWAITING_APPROVAL; retain paused for summary fields. Apply the same predicate
distinction in McpHitlTools.getGroupApprovalStatus and add a regression test
covering an approver during AWAITING_HUMAN_INPUT.

In
`@src/test/java/ai/labs/eddi/engine/internal/GroupConversationServiceExtendedTest.java`:
- Around line 171-183: Update midRepeatHumanPause_persistsTheRepeatSliceBase to
capture the GroupConversation passed to conversationStore.update(...) during
service.discuss(), then assert pausedRepeatSliceBase on that persisted argument
rather than only on the returned instance. If the persistence suite has Mongo
serialization coverage, add a round-trip assertion for the same field.

---

Outside diff comments:
In `@src/main/java/ai/labs/eddi/engine/hitl/HitlAccessGuard.java`:
- Around line 159-160: Update listGroupPendingApprovals so the callerId
predicate is applied in the scoped service/store query before limiting results;
add or reuse a query filtering ownerId or pendingMemberId, then apply limit to
the filtered results so the assigned pending member’s approvals are included.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f722b22a-3150-42db-93c4-484f00aae513

📥 Commits

Reviewing files that changed from the base of the PR and between 9c84783 and ab5c115.

📒 Files selected for processing (18)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/configs/groups/model/GroupConversation.java
  • src/main/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStore.java
  • src/main/java/ai/labs/eddi/engine/hitl/HitlAccessGuard.java
  • src/main/java/ai/labs/eddi/engine/hitl/HitlCrashRecoveryObserver.java
  • src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/RestGroupConversation.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinator.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/GroupLifecycleOps.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpHitlTools.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpToolFilter.java
  • src/main/java/ai/labs/eddi/integrations/slack/SlackGroupDiscussionListener.java
  • src/test/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStoreTest.java
  • src/test/java/ai/labs/eddi/engine/hitl/HitlAccessGuardTest.java
  • src/test/java/ai/labs/eddi/engine/internal/GroupConversationServiceExtendedTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpHitlToolsCoverageTest.java
  • src/test/java/ai/labs/eddi/engine/mcp/McpHitlToolsTest.java
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/main/java/ai/labs/eddi/integrations/slack/SlackGroupDiscussionListener.java
  • src/test/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStoreTest.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/GroupLifecycleOps.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpHitlTools.java
  • src/main/java/ai/labs/eddi/engine/hitl/HitlCrashRecoveryObserver.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java
  • src/main/java/ai/labs/eddi/configs/groups/mongo/AgentGroupStore.java
  • src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/GroupHitlCoordinator.java

Comment thread src/main/java/ai/labs/eddi/engine/internal/RestGroupConversation.java Outdated
…rsist-time test (review round)

- CURRENT_SCHEMA_VERSION 3 -> 4: pausedRepeatSliceBase is resume-consumed
  persisted state; legacy docs default to -1 via Jackson (no migration)
- the approver detail=full window now requires AWAITING_APPROVAL on both
  REST and MCP surfaces — the shared paused predicate also admitted
  approvers to the transcript during AWAITING_HUMAN_INPUT
- the mid-repeat pause test records pausedRepeatSliceBase at persist time
  inside the update() stub instead of asserting the mutable instance
@aisabella-ai
aisabella-ai self-requested a review August 8, 2026 08:09
@ginccc
ginccc merged commit 492a347 into main Aug 8, 2026
30 checks passed
@ginccc
ginccc deleted the feat/group-i6-human-members branch August 8, 2026 09:29
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.

3 participants