Skip to content

refactor: Wave R — decompose GroupConversationService, AgentOrchestrator and ConversationService - #626

Merged
ginccc merged 64 commits into
mainfrom
refactor/group-service-split
Aug 7, 2026
Merged

refactor: Wave R — decompose GroupConversationService, AgentOrchestrator and ConversationService#626
ginccc merged 64 commits into
mainfrom
refactor/group-service-split

Conversation

@ginccc

@ginccc ginccc commented Aug 1, 2026

Copy link
Copy Markdown
Member

Summary

Wave R of planning/group-collaboration-improvements-plan.md is complete. All three god-classes are decomposed, ahead of the ~18 feature items the plan lands on top of them.

Class Before After
GroupConversationService 4,417 1,422
AgentOrchestrator 2,725 1,163
ConversationService 2,735 1,174

Checkstyle went 8 violations → 6, and every remaining one is a pre-existing LineLength in a file this branch never touched. There is no FileLength violation left anywhere in the codebase.

Every extraction is a behaviour-preserving pure move. The three classes remain the facades (IGroupConversationService / IConversationService unchanged) and delegate. Many methods stay as thin declared delegators specifically because characterization tests reach them via getDeclaredMethod(...), which resolves by exact parameter types — removing them breaks the safety net even though nothing in production would notice. Per plan rule 3.0-4, collaborators are plain classes constructed by the facade, not CDI beans, because ~34 test classes construct these services directly.

R1 — GroupConversationService → 8 collaborators

GroupAttachmentBinder · GroupContextBuilder · GroupSigningGuard · MemberTurnExecutor · PhaseExecutionEngine · TaskForceEngine · GroupHitlCoordinator · GroupLifecycleOps

Plus one deliberate behaviour change in its own commit (plan rule 3.0-1): group discussions now participate in graceful shutdown via rejectIfShuttingDown() on every entry point that starts, continues or resumes a discussion → RejectedExecutionException, already mapped to HTTP 503.

R2 — AgentOrchestrator → the ToolSourceProvider SPI

All eight tool sources are providers now, and buildToolSetup iterates them rather than calling each by name. Adding a tool source is adding a provider — the property that gates Wave 2's I5/I7/I17.

BuiltinToolsProvider · ContextualToolsProvider · AttachmentToolsProvider · DynamicAgentToolsProvider · HttpCallToolsProvider · McpToolsProvider · A2AToolsProvider, assembled by ToolSourceRegistry, with ToolContextBudget, ToolApprovalGateSupport, ToolLoopRunner, ToolLoopResumer and the IAgentOrchestrator interface alongside.

Three properties worth calling out:

  • Isolation is structural. One failing source used to abort assembly for every other — an unreachable MCP server cost the agent its calculator. ToolSourceRegistry wraps every contribute call, catching Throwable (the realistic non-Exception is NoClassDefFoundError from an optional integration whose dependency is absent at runtime).
  • Merge is deterministic. First-write-wins per dispatch name, so collisions resolve by provider order rather than map iteration order, and an operator cannot shadow a governed built-in by naming an MCP tool after it.
  • Per-tool provenance survives. toolSources tags come from the tool's own class, not the provider's source() — stamping one tag over a contribution that legitimately spans memory/recall/builtin would silently unmatch a require: ["memory:*"] approval pattern.

R3 — ConversationServiceConversationHitlService + ConversationStepRunner

The HITL cluster was the back ~43% and had almost nothing to do with the front. What remained was two things wearing one name: the public IConversationService surface, and the machinery that executes a turn.

Bugs found and fixed along the way

  • Key-rotation-unsafe signature verification (GroupSigningGuard) — resolved a signer's public key via "whichever key is valid right now" instead of the exact version a signature declares, at both call sites. During a rotation overlap window (a scenario the key model explicitly supports) this could self-discard a good signature or verify against the wrong key. The peer-verify cache was also keyed by agent id alone, so a second entry signed with a different key version reused the first entry's cached key.
  • Unsigned member contributions (MemberTurnExecutor) — when a group auto-rejected a member's gated tool call, the resulting transcript entry was built without a signature, while the normal path three lines away signed an identically-shaped entry that peers verify identically.
  • Task verification could abort entirely (TaskForceEngine) — a verifier LLM repeating a subject re-verified an already-verified task, and the resulting IllegalStateException escaped from a loop sitting outside the enclosing try, losing the verifier's transcript entry and its event.
  • A throwing cancel listener left dynamic agents deployed (GroupHitlCoordinator) — notifyCancelled ran inside the state-reverting catch, so an SSE sink on a closed stream desynced memory from the store, and the cleanup path reads memory.
  • snippets/vars unprotected (HttpCallToolsProvider, reported by Copilot) — a prompt-injected tool argument could shadow the deployment-config namespace that httpcall templates read.
  • Per-turn dynamic-agent capmaxCreatedAgentsPerDiscussion started empty on every buildToolList call, so a 5-member × 3-phase discussion with the default cap of 5 could deploy up to 75 agents to production.
  • Plus an NPE in GroupSigningGuard, missing enableBuiltInTools gates on two providers, a null-returning extractText dereferenced inside a keep-the-turn-alive fallback, and three logging/privacy fixes.

A graceful-shutdown observer was added and then removed, not fixed: GracefulShutdownService sets shuttingDown = true inside drain(), so an earlier-priority observer runs while the reject gate is still open and races the very drain it was meant to precede.

Review

Independent multi-agent reviews at four points, plus every automated comment on this PR. All findings are fixed in-branch — see the fix(review): commits and changelog entries. Beyond the test suite, the extractions were verified structurally: every pre-branch method signature matched against the union of facade + collaborators (none vanished), and a comment-stripped line diff resolved every unmatched statement to an intentional qualifier rewrite.

Two findings were declined with reasons: gating create_sub_agent/teardown_agent at registration (the guardrails are already enforced in the tool bodies, and suppressing registration is a behaviour change inside a pure move), and removing six "unused" parameters (those signatures are pinned by 44 reflective test references that resolve on exact parameter types — the declarations now carry a comment explaining why the obvious fix breaks the build).

Test plan

  • ./mvnw clean compile — clean
  • 2,640 tests green across every group, orchestrator, conversation and HITL battery
  • 12 new focused collaborator test classes, including ToolSourceProviderTest (the plan's R2 post-condition: a provider throwing yields an empty contribution and the loop continues)
  • ./mvnw formatter:format validate — clean; 6 pre-existing LineLength violations remain, all in untouched files
  • Every behavioural fix mutation-checked: reverting each fails exactly its own test
  • CI green — Build & Test, Integration Tests, CodeQL, Trivy, Fuzz, Secret Scanning, Preflight

What's next

Wave 0 foundations (F1–F6) and the feature items (I1–I18). Full sequencing in the plan's §7 dependency graph. Follow-ups recorded as separate tasks rather than folded in here: group-signing replay protection, requirePeerVerification being audit-only, whether a moderator should be peer-critiqued, and a TaskForceEngine lock-ordering concurrency test.

ginccc added 4 commits August 1, 2026 18:30
…nService

Add the Rev 2.1 group-collaboration implementation plan and begin its Wave R
refactoring workstream: GroupConversationService (4,417 lines) is decomposed
into focused collaborator classes before the plan's ~18 feature items land,
so they get clean homes instead of piling onto an already-oversized file.

R1 step 1 (smallest, pure-move extraction first per the plan's ordering):
move the attachment cluster (materializeAttachments, rehydrateAttachmentsFromStore,
grantAndInjectAttachments) into a new GroupAttachmentBinder. Plain class, not a
CDI bean, constructed per call site — the existing test suite constructs
GroupConversationService directly and attachmentStore is field-injected
specifically to keep that compiling, so the extraction must not change the
constructor signature.

Baselined the full 12-class/470-test GroupConversationService*Test suite
before touching code (all green, 82% instruction / 72% branch JaCoCo) as the
refactor's regression budget. Moved the 12 dedicated attachment tests to a
new focused GroupAttachmentBinderTest testing the extracted class directly.
Re-verified: still 470 tests total, all green, clean compile, clean
formatter/Checkstyle.
…ervice

R1 step 2 of planning/group-collaboration-improvements-plan.md: move the
phase-input-construction and scope-filtering cluster (buildPhaseInput,
selectDefaultTemplate, filterByScope, findLatestResponse, mapPhaseToEntryType,
extractResponse, buildPlainTextFallback) into a new GroupContextBuilder,
constructed once in GroupConversationService's constructor.

Several of these methods are reached by characterization tests via
GroupConversationService.class.getDeclaredMethod(...) reflection, which
requires the method to stay declared directly on that class. Kept all seven
as thin private delegators rather than inlining at call sites, after an
exhaustive sweep of every reflection lookup across the test package.

Added a focused GroupContextBuilderTest (direct construction, no reflection)
alongside the untouched characterization suites. Full group suite + both new
test classes green; clean compile; formatter/Checkstyle clean.
…vice

R1 step 3 of planning/group-collaboration-improvements-plan.md: move the
Ed25519 inter-agent signing cluster into a new GroupSigningGuard --
verifyPriorEntriesIfRequired, the signing-creation block that was inline
inside executeAgentTurn, and the lastVerifiedIndex cursor map both share.

The signing block's four loose local variables (signature, nonce,
timestampMs, keyVersion) become a SigningResult record with an UNSIGNED
singleton for "not signed, for any reason". verifyPriorEntriesIfRequired
stays a declared delegator (reflection dependency, same pattern as step 2).

Added a focused GroupSigningGuardTest covering the guard-clause branches
directly; the full sign/verify/nonce-validate happy path needs real Ed25519
key material and stays covered by the untouched characterization suites.

Full group suite + all three new focused test classes green; clean compile;
formatter/Checkstyle clean.
Independent 5-agent review of the 3 R1 extraction commits before push
surfaced one real pre-existing bug worth fixing in place, plus five stale
comments left behind by the moves.

GroupSigningGuard resolved a signer's public key via getKeyValidAt(timestamp)
-- "whichever key is valid right now" -- at both the self-verify-after-signing
and peer-verify-on-receipt call sites, instead of getKeyForVersion(exactVersion)
using the key version actually recorded on the signature. During a key
rotation's overlap window (both old and new key simultaneously valid -- the
scenario AgentPublicKey is explicitly designed to support) this could
self-discard a good signature, or verify an entry against the wrong key
entirely. The peer-verify cache was also keyed by agent ID alone, so a second
entry from the same agent signed with a different key version silently reused
the first entry's cached key without ever consulting its own key version.

Fixed both lookups; changed the verify-side cache key to agentId#keyVersion.
Added two regression tests that construct an overlapping-validity two-key
identity and assert the exact key material passed to verifyEnvelope via
ArgumentCaptor. Mutation-checked: temporarily reverted the production fix via
git stash on just that file, confirmed both new tests fail and only those
two, then restored it.

Two related pre-existing findings (no replay check on the verify path;
requirePeerVerification is audit-only) were deliberately not fixed here --
both are security-sensitive design decisions, not mechanical bugs, and are
filed as separate follow-up tasks with full context instead.

Also fixed: an inline FQN in GroupSigningGuardTest (AGENTS.md Sec4.4), a
stale "is now private" comment on a method that's actually public, two
Javadoc comments still naming lastVerifiedIndex after it moved into
GroupSigningGuard, and an overstated test-coverage claim in a class Javadoc.

Full 15-class group suite green; clean compile; formatter/Checkstyle clean.
@ginccc
ginccc requested a review from rolandpickl as a code owner August 1, 2026 17:29
Copilot AI review requested due to automatic review settings August 1, 2026 17:29
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change extracts group discussion and LLM tool orchestration responsibilities into dedicated components. It adds focused tests for execution, lifecycle, signing, attachments, providers, and budgeting. It also updates the changelog and implementation plan.

Changes

Group collaboration refactor

Layer / File(s) Summary
Group helper components
src/main/java/ai/labs/eddi/engine/internal/groups/*, src/test/java/ai/labs/eddi/engine/internal/groups/*Test.java
Adds attachment binding, context construction, signing, peer verification, and focused tests.
Member-turn execution
src/main/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutor.java
Executes agent and nested-group turns with retries, timeouts, cancellation, attachments, signing, HITL handling, and failure policies.
Phase and task-force execution
src/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java, src/main/java/ai/labs/eddi/engine/internal/groups/TaskForceEngine.java
Adds sequential, parallel, peer-targeted, and task-force execution with budgets, dependency handling, verification, cancellation, recovery, and assignment.
HITL and lifecycle integration
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/GroupConversationService.java
Delegates pause, resume, cancellation, lifecycle, cleanup, shutdown rejection, and execution operations.
Planning and change records
planning/group-collaboration-improvements-plan.md, docs/changelog.md
Documents the revised plan, extracted components, validation, scope, and follow-up work.

LLM tool orchestration

Layer / File(s) Summary
Tool provider contracts
src/main/java/ai/labs/eddi/modules/llm/tools/spi/*
Adds ToolSourceProvider, ToolContribution, ToolAssemblyContext, and ProviderFailure.
Tool providers and utilities
src/main/java/ai/labs/eddi/modules/llm/impl/*, src/main/java/ai/labs/eddi/modules/llm/impl/orchestration/ToolContextBudget.java
Adds contextual, dynamic-agent, A2A, HTTP, and MCP providers, reflection support, and tool-context budgeting.
AgentOrchestrator integration
src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java, src/main/java/ai/labs/eddi/modules/llm/impl/LlmTask.java, src/main/java/ai/labs/eddi/modules/llm/impl/TokenCounterFactory.java
Wires extracted providers and budgeting into the orchestrator while retaining compatibility delegators and public helper methods.

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

Sequence Diagram(s)

sequenceDiagram
  participant GroupConversationService
  participant PhaseExecutionEngine
  participant TaskForceEngine
  participant MemberTurnExecutor
  participant GroupContextBuilder
  participant GroupSigningGuard
  participant ConversationService
  GroupConversationService->>PhaseExecutionEngine: execute discussion phase
  GroupConversationService->>TaskForceEngine: execute task phase
  PhaseExecutionEngine->>MemberTurnExecutor: execute member turn
  MemberTurnExecutor->>GroupContextBuilder: build member input
  MemberTurnExecutor->>GroupSigningGuard: verify and sign messages
  MemberTurnExecutor->>ConversationService: execute private or nested conversation
  TaskForceEngine->>MemberTurnExecutor: execute assigned task
Loading

Possibly related PRs

  • labsai/EDDI#572: Adds the task-force behavior later extracted into TaskForceEngine.
  • labsai/EDDI#604: Modifies AgentOrchestrator and related tool wiring.
  • labsai/EDDI#619: Adds group cancellation and graceful-shutdown behavior used by this refactor.

Suggested reviewers: rolandpickl, copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.34% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately identifies the main Wave R refactoring of GroupConversationService and AgentOrchestrator, although ConversationService is not changed in the provided summary.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch refactor/group-service-split
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/group-service-split

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.

return result != null ? result : "";
}

public String buildPlainTextFallback(DiscussionPhase phase, GroupMember speaker, String question, List<TranscriptEntry> transcript) {

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR begins decomposing the GroupConversationService god-class into smaller, independently testable collaborators (Wave R / R1 steps 1–3), while keeping IGroupConversationService as the stable facade and preserving characterization-test reflection entry points. It also adds the Rev 2.1 implementation plan for upcoming group-collaboration features and documents the refactor/bugfix work in the changelog.

Changes:

  • Extracted attachment handling, phase-input/context building, and signing/peer-verification logic into GroupAttachmentBinder, GroupContextBuilder, and GroupSigningGuard, with GroupConversationService delegating.
  • Added focused unit tests for the extracted collaborators and moved the attachment tests out of GroupConversationServiceTest.
  • Added planning/group-collaboration-improvements-plan.md and updated docs/changelog.md with the refactor steps and signing verification fix context.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java Replaces inline attachment/context/signing clusters with delegations to extracted collaborators; cleans up signing-cursor cleanup to call signingGuard.forgetConversation.
src/main/java/ai/labs/eddi/engine/internal/groups/GroupAttachmentBinder.java New helper for attachment materialize/rehydrate/grant+inject behavior formerly inside the facade.
src/main/java/ai/labs/eddi/engine/internal/groups/GroupContextBuilder.java New helper for building per-phase input and transcript scoping; extracted from the facade.
src/main/java/ai/labs/eddi/engine/internal/groups/GroupSigningGuard.java New helper encapsulating inter-agent signing + incremental peer verification + verification cursor state.
src/test/java/ai/labs/eddi/engine/internal/GroupConversationServiceTest.java Removes the attachment nested test class (moved to a dedicated binder test).
src/test/java/ai/labs/eddi/engine/internal/groups/GroupAttachmentBinderTest.java New focused tests for GroupAttachmentBinder.
src/test/java/ai/labs/eddi/engine/internal/groups/GroupContextBuilderTest.java New focused tests for GroupContextBuilder.
src/test/java/ai/labs/eddi/engine/internal/groups/GroupSigningGuardTest.java New focused tests for GroupSigningGuard, including key-rotation regression coverage.
planning/group-collaboration-improvements-plan.md Adds the Rev 2.1 plan and dependency graph for the broader group-collaboration roadmap.
docs/changelog.md Documents the refactor steps and the signing verification/key-rotation fix findings.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +87 to +93
var resourceId = agentStore.getCurrentResourceId(agentId);
var agentConfig = agentStore.read(agentId, resourceId.getVersion());
if (agentConfig.getSecurity() == null
|| !agentConfig.getSecurity().isSignInterAgentMessages()
|| response == null) {
return SigningResult.UNSIGNED;
}
Comment on lines +134 to +139
case PLAN -> {
// Provide member list for planning template
List<Map<String, Object>> memberList = new ArrayList<>();
// Note: speaker list should be the full member list for planning
data.put("members", memberList); // populated by caller via template data
}

@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: 4

🧹 Nitpick comments (3)
src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java (2)

409-411: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid allocating a GroupAttachmentBinder per call.

attachmentBinder() builds a new instance on every attachment operation, including grantAndInjectAttachments, which runs on each member's first turn. The class is stateless, so the allocation buys nothing. The stated reason is that attachmentStore is field-injected and test-mutable.

Read the field lazily inside a single cached binder instead, or inject the store through the constructor together with the other collaborators. See the related comment on the constructor.

🤖 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/GroupConversationService.java`
around lines 409 - 411, Cache a single stateless GroupAttachmentBinder instead
of constructing one in every attachment operation. Update attachmentBinder() to
lazily initialize and reuse the binder while reading the current attachmentStore
field, preserving test-time field mutability; alternatively, move
attachmentStore to constructor injection as requested by the related constructor
comment and initialize the binder once.

236-237: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wire the three new collaborators as CDI beans. All three extracted components are instantiated by hand inside GroupConversationService, so the container manages neither their lifetime nor their dependencies. The repository guidelines require Quarkus CDI for components. Annotating each class with @ApplicationScoped and an @Inject constructor keeps the direct-construction unit tests working, because a CDI constructor is still a plain Java constructor.

  • src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java#L236-L237: replace the two new calls with injected GroupContextBuilder and GroupSigningGuard fields. This matters most for GroupSigningGuard, which owns the lastVerifiedIndex cursor and therefore must exist as exactly one instance.
  • src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java#L409-L411: replace the per-call attachmentBinder() factory with a single injected GroupAttachmentBinder, or have the binder read the injected IAttachmentStore lazily so no instance is allocated on each member turn.

As per coding guidelines: "Use Quarkus CDI (@ApplicationScoped and @Inject) for components; do not manually register modules."

🤖 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/GroupConversationService.java`
around lines 236 - 237, Make GroupContextBuilder, GroupSigningGuard, and
GroupAttachmentBinder CDI-managed with `@ApplicationScoped` and `@Inject`
constructors, preserving direct-construction tests. In
GroupConversationService.java lines 236-237, inject and reuse
GroupContextBuilder and GroupSigningGuard instead of constructing them; at lines
409-411, inject and reuse one GroupAttachmentBinder rather than creating it per
call, with lazy IAttachmentStore access if needed.

Source: Coding guidelines

src/test/java/ai/labs/eddi/engine/internal/groups/GroupContextBuilderTest.java (1)

180-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the selected template, not just non-null.

These four tests name a specific branch of selectDefaultTemplate, but assertNotNull passes for every branch. If the ContextScope.NONE and ContextScope.ANONYMOUS branches were swapped, all four tests would still pass. Compare against the expected DiscussionStylePresets constant instead.

💚 Proposed assertions
     `@Test`
     void selectDefaultTemplate_opinionNone_independentTemplate() {
-        assertNotNull(builder.selectDefaultTemplate(phase(PhaseType.OPINION, ContextScope.NONE), List.of(), 0));
+        assertEquals(DiscussionStylePresets.TEMPLATE_OPINION_INDEPENDENT,
+                builder.selectDefaultTemplate(phase(PhaseType.OPINION, ContextScope.NONE), List.of(), 0));
     }
 
     `@Test`
     void selectDefaultTemplate_opinionAnonymous_anonymousTemplate() {
-        assertNotNull(builder.selectDefaultTemplate(phase(PhaseType.OPINION, ContextScope.ANONYMOUS), List.of(), 0));
+        assertEquals(DiscussionStylePresets.TEMPLATE_OPINION_ANONYMOUS,
+                builder.selectDefaultTemplate(phase(PhaseType.OPINION, ContextScope.ANONYMOUS), List.of(), 0));
     }
 
     `@Test`
     void selectDefaultTemplate_opinionFull_withContextTemplate() {
-        assertNotNull(builder.selectDefaultTemplate(phase(PhaseType.OPINION, ContextScope.FULL), List.of(), 0));
+        assertEquals(DiscussionStylePresets.TEMPLATE_OPINION_WITH_CONTEXT,
+                builder.selectDefaultTemplate(phase(PhaseType.OPINION, ContextScope.FULL), List.of(), 0));
     }
 
     `@Test`
     void selectDefaultTemplate_nonOpinion_usesPresetDefault() {
-        assertNotNull(builder.selectDefaultTemplate(phase(PhaseType.CRITIQUE, ContextScope.FULL), List.of(), 0));
+        assertEquals(DiscussionStylePresets.defaultTemplate(PhaseType.CRITIQUE),
+                builder.selectDefaultTemplate(phase(PhaseType.CRITIQUE, ContextScope.FULL), List.of(), 0));
     }

Add the import for ai.labs.eddi.configs.groups.model.DiscussionStylePresets.

🤖 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/GroupContextBuilderTest.java`
around lines 180 - 198, Update the four selectDefaultTemplate tests in
GroupContextBuilderTest to assert equality with the expected
DiscussionStylePresets constant for each PhaseType and ContextScope branch,
replacing assertNotNull. Add the DiscussionStylePresets import and use the
independent, anonymous, contextual, and preset-default constants corresponding
to the tested branches.
🤖 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 `@docs/changelog.md`:
- Line 49: Update the characterization test class count and list in the
changelog entry so they agree: either add the omitted class name to the list or
change “four” to “three,” preserving the documented reflection behavior.

In `@planning/group-collaboration-improvements-plan.md`:
- Line 61: Update the signing summary near executeAgentTurn to qualify
NonceCacheService as sender-side nonce validation only. Remove the implication
that receipt verification currently provides replay protection, and mention
replay detection during verification as follow-up work.
- Around line 138-140: The dependency map for GroupAttachmentBinder,
GroupContextBuilder, and GroupSigningGuard does not match their extracted
constructors. Update the plan to list GroupAttachmentBinder as depending on
attachmentStore and defaultTenantId, GroupContextBuilder only on
templatingEngine, and GroupSigningGuard on agentStore, agentSigningService,
nonceCacheService, and defaultTenantId; remove IJsonSerialization from the first
two entries.

In `@src/main/java/ai/labs/eddi/engine/internal/groups/GroupSigningGuard.java`:
- Around line 249-267: Update the peer-verification flow in GroupSigningGuard
around the publicKeyCache lookup to detect a null entry.signatureKeyVersion
before constructing cacheKey or calling getKeyForVersion. Treat entries with
missing signature key versions as unsigned/unresolvable without incrementing
failed++, while preserving the intentional zero-version behavior when key
metadata is absent.

---

Nitpick comments:
In `@src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java`:
- Around line 409-411: Cache a single stateless GroupAttachmentBinder instead of
constructing one in every attachment operation. Update attachmentBinder() to
lazily initialize and reuse the binder while reading the current attachmentStore
field, preserving test-time field mutability; alternatively, move
attachmentStore to constructor injection as requested by the related constructor
comment and initialize the binder once.
- Around line 236-237: Make GroupContextBuilder, GroupSigningGuard, and
GroupAttachmentBinder CDI-managed with `@ApplicationScoped` and `@Inject`
constructors, preserving direct-construction tests. In
GroupConversationService.java lines 236-237, inject and reuse
GroupContextBuilder and GroupSigningGuard instead of constructing them; at lines
409-411, inject and reuse one GroupAttachmentBinder rather than creating it per
call, with lazy IAttachmentStore access if needed.

In
`@src/test/java/ai/labs/eddi/engine/internal/groups/GroupContextBuilderTest.java`:
- Around line 180-198: Update the four selectDefaultTemplate tests in
GroupContextBuilderTest to assert equality with the expected
DiscussionStylePresets constant for each PhaseType and ContextScope branch,
replacing assertNotNull. Add the DiscussionStylePresets import and use the
independent, anonymous, contextual, and preset-default constants corresponding
to the tested branches.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 496fa6ec-49b1-4e71-93cd-24e8d4dbf202

📥 Commits

Reviewing files that changed from the base of the PR and between 2d675bc and 8b76d74.

📒 Files selected for processing (10)
  • docs/changelog.md
  • planning/group-collaboration-improvements-plan.md
  • src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/GroupAttachmentBinder.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/GroupContextBuilder.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/GroupSigningGuard.java
  • src/test/java/ai/labs/eddi/engine/internal/GroupConversationServiceTest.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/GroupAttachmentBinderTest.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/GroupContextBuilderTest.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/GroupSigningGuardTest.java

Comment thread docs/changelog.md Outdated
Comment thread planning/group-collaboration-improvements-plan.md Outdated
Comment thread planning/group-collaboration-improvements-plan.md
…rvice

R1 step 4 of planning/group-collaboration-improvements-plan.md -- the biggest
extraction yet, and the one GroupConversationServiceConcurrencyTest exists
specifically to pin. Moves both executeAgentTurn overloads,
tryResolveMemberToolPause, handleMemberPause, executeGroupMemberTurn,
handleAgentFailure and errorEntry into a new MemberTurnExecutor.

Three design decisions resolved during scoping:
- executeGroupMemberTurn recurses into the facade's own public discuss()/
  cancelDiscussion() for nested GROUP-type members. Resolved by passing
  `this` into MemberTurnExecutor's constructor (safe: it only stores the
  reference, never invokes it during construction).
- propagateDynamicAgentTracking stays on GroupConversationService (widened to
  public static) rather than moving -- DynamicAgentTrackingPropagationTest
  calls it directly by class name, not via reflection, and the plan already
  assigns it to a later step (GroupLifecycleOps).
- Attachment granting reaches back through the same self-reference to the
  facade's grantAndInjectAttachments (widened to public) rather than giving
  MemberTurnExecutor its own IAttachmentStore and duplicating the facade's
  per-call GroupAttachmentBinder construction.

Also widened MemberTurnCancellation/MemberTurnCancelledException to public
(MemberTurnExecutor lives in a different package and references them in its
own signatures).

The reflection sweep needed a second pass -- the first only grepped
getDeclaredMethod("...") and missed this codebase's actual method("...")
test-helper-wrapper pattern, which turned up real dependencies in two files
the first pass said were clean. All six moved methods kept as thin
delegators as a result.

Full 12-class group suite + DynamicAgentTrackingPropagationTest (22 tests,
unmodified) + 4 focused collaborator test classes green on the first run
after compile succeeded, including GroupConversationServiceConcurrencyTest
(8/8). Added a focused MemberTurnExecutorTest for the pure-function methods.

GroupConversationService: 3,977 -> 3,605 lines.
Copilot AI review requested due to automatic review settings August 1, 2026 17:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/main/java/ai/labs/eddi/engine/internal/groups/GroupSigningGuard.java:90

  • signOutgoingMessage() fetches the agent resource/config before checking for a null response, and it assumes agentStore.getCurrentResourceId(agentId) is non-null. When response is null (or the agent has no current resource id / config), this does unnecessary store IO and typically falls into the catch-all warning path, producing noisy logs for a normal “unsigned” outcome. Add early returns for response==null and resourceId/config==null, and keep the security flag check after those guards.
        try {
            var resourceId = agentStore.getCurrentResourceId(agentId);
            var agentConfig = agentStore.read(agentId, resourceId.getVersion());
            if (agentConfig.getSecurity() == null
                    || !agentConfig.getSecurity().isSignInterAgentMessages()

docs/changelog.md:12

  • PR description/title says this PR covers R1 steps 1–3, but this changelog entry states it includes R1 step 4 (MemberTurnExecutor extraction). Please align the PR metadata (title/description) with the actual scope of changes, or split step 4 into its own PR if the intent was to keep this PR limited to steps 1–3.

@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: 1

🧹 Nitpick comments (2)
src/main/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutor.java (1)

549-552: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Chain the cause into the thrown GroupDiscussionException.

This throw drops cause, so the original stack trace is lost for every ABORT failure, including sub-group failures routed here from Line 541. Lines 160 and 179 already use the cause-carrying constructor of the same exception type. cause.getMessage() can also be null for exceptions without a message.

♻️ Proposed refactor to preserve the cause
         if (protocol.onAgentFailure() == ProtocolConfig.MemberFailurePolicy.ABORT) {
             throw new GroupDiscussionException(
-                    "%s for agent %s and onAgentFailure=ABORT: %s".formatted(prefix, member.agentId(), cause.getMessage()));
+                    "%s for agent %s and onAgentFailure=ABORT: %s".formatted(prefix, member.agentId(), cause.getMessage()), cause);
         }
🤖 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 549 - 552, Update the ABORT branch in MemberTurnExecutor’s failure
handling to construct GroupDiscussionException with cause as the chained
throwable, preserving the existing context message while retaining failures with
null messages. Match the cause-carrying constructor usage already established at
the other GroupDiscussionException call sites.
src/test/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutorTest.java (1)

44-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer mocks over null for the helper collaborators.

GroupSigningGuard and GroupContextBuilder receive null dependencies. The four current tests exercise errorEntry and handleAgentFailure, which touch neither the agent store nor the templating engine, so the fixture works today. If a later change makes either method reach a collaborator, the test fails with a bare NullPointerException instead of an unmet-stub message. Mocks keep the failure diagnosable and let the fixture be reused for more paths.

♻️ Proposed fixture change
     private MemberTurnExecutor executor() {
         return new MemberTurnExecutor(
                 Mockito.mock(IConversationService.class),
                 Mockito.mock(IAgentFactory.class),
-                new GroupSigningGuard(null, null, null, "default"),
-                new GroupContextBuilder(null),
+                new GroupSigningGuard(
+                        Mockito.mock(IAgentStore.class),
+                        Mockito.mock(AgentSigningService.class),
+                        Mockito.mock(NonceCacheService.class),
+                        "default"),
+                new GroupContextBuilder(Mockito.mock(ITemplatingEngine.class)),
                 Mockito.mock(GroupConversationService.class),
                 new SimpleMeterRegistry().counter("test.member.pause.skipped"),
                 180, 2);
     }

Add the matching imports for IAgentStore, AgentSigningService, NonceCacheService, and ITemplatingEngine.

🤖 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/MemberTurnExecutorTest.java`
around lines 44 - 53, Update the executor() test fixture to replace the null
dependencies passed to GroupSigningGuard and GroupContextBuilder with Mockito
mocks of IAgentStore, AgentSigningService, NonceCacheService, and
ITemplatingEngine, adding the corresponding imports while preserving the
existing constructor setup.
🤖 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/engine/internal/groups/MemberTurnExecutor.java`:
- Around line 444-455: Update the graceful resume path in MemberTurnExecutor
around the tool-less contribution construction to call
signingGuard.signOutgoingMessage for the generated response, then populate the
returned signature, nonce, timestampMs, and keyVersion in TranscriptEntry.
Preserve the existing UNSIGNED behavior when signing is disabled and keep the
current response fallback and logging unchanged.

---

Nitpick comments:
In `@src/main/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutor.java`:
- Around line 549-552: Update the ABORT branch in MemberTurnExecutor’s failure
handling to construct GroupDiscussionException with cause as the chained
throwable, preserving the existing context message while retaining failures with
null messages. Match the cause-carrying constructor usage already established at
the other GroupDiscussionException call sites.

In
`@src/test/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutorTest.java`:
- Around line 44-53: Update the executor() test fixture to replace the null
dependencies passed to GroupSigningGuard and GroupContextBuilder with Mockito
mocks of IAgentStore, AgentSigningService, NonceCacheService, and
ITemplatingEngine, adding the corresponding imports while preserving the
existing constructor setup.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e2d6de6-128e-4b1f-8df9-2ee08d9c5748

📥 Commits

Reviewing files that changed from the base of the PR and between 8b76d74 and 741bc3f.

📒 Files selected for processing (4)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutor.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/MemberTurnExecutorTest.java

…Service

R1 step 5 of planning/group-collaboration-improvements-plan.md: move the
debate-style turn-order executors -- executeSequentialPhase,
executeParallelPhase, executePeerTargetedPhase -- into a new
PhaseExecutionEngine. TASK_FORCE's PLAN/EXECUTE/VERIFY routing stays on the
facade (separate cluster, R1 step 6).

Deliberately skipped the plan's speculative PhaseExecutor/PhaseOutcome/
PhaseExitSignal interface -- that shape assumes F2/ResumePoint and
I2/convergence, neither of which exists yet since R1 runs before Wave 0/1.
Moved the three methods as concrete methods on a plain class; the interface
is a decision for whenever F2/I2 actually need it.

PhaseExecutionEngine takes the facade's ExecutorService by reference, not
ownership (TaskForceEngine's not-yet-extracted waves share it; the facade
keeps the @PreDestroy shutdown hook). parallelBatchBudgetSeconds stays on
the facade, widened to public static, called back cross-package.

Reflection sweep found a third distinct test-helper-wrapper naming
convention (phaseMethod(name), case-sensitively distinct from the method(name)
pattern used elsewhere) that broke the first test run with a
NoSuchMethodException. Fixed by re-sweeping with a bare-token grep instead
of guessing at wrapper names -- adopting that as the standard approach going
forward. executeParallelPhase kept as a delegator as a result;
executeSequentialPhase/executePeerTargetedPhase confirmed to have no test
dependency and were fully inlined.

Also caught and fixed a bug in the new PhaseExecutionEngineTest itself (a
mock stub hardcoding targetAgentId=null instead of threading through the
real argument) before it ever reached CI.

Full 12-class group suite + DynamicAgentTrackingPropagationTest + 5 focused
collaborator test classes green (540 tests).

GroupConversationService: 3,605 -> 3,432 lines.
Copilot AI review requested due to automatic review settings August 1, 2026 18:31

@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: 1

🧹 Nitpick comments (2)
src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java (1)

254-262: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Confirm the manual construction of the helper components is intended.

GroupContextBuilder, GroupSigningGuard, MemberTurnExecutor, and now PhaseExecutionEngine are constructed with new inside the constructor. The coding guidelines require Quarkus CDI for components. The comments explain that direct construction keeps the existing direct-construction unit tests working, so this looks deliberate. If CDI beans are planned for a later wave, record that follow-up in the plan document so the deviation stays visible.

As per coding guidelines: "Use Quarkus CDI (@ApplicationScoped and @Inject) for components; do not manually register modules."

🤖 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/GroupConversationService.java`
around lines 254 - 262, Confirm that manual construction of GroupContextBuilder,
GroupSigningGuard, MemberTurnExecutor, and PhaseExecutionEngine is intentional,
and document the CDI migration as a follow-up in the plan document if it is
deferred. Keep the existing direct-construction behavior and unit-test
compatibility unchanged.

Source: Coding guidelines

src/test/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngineTest.java (1)

130-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a partial parallel-budget test.

parallelPhase_zeroRemainingBudget_noop covers only the fully exhausted budget. The subList(0, min(speakers.size(), remainingTurns)) cap at PhaseExecutionEngine lines 100-102 stays untested, and it is the branch where an off-by-one would appear. Add a case with a partial budget, for example 3 speakers and maxTurns 2 with turnCounter 0, then assert two transcript entries and turnCounter equal to 2.

♻️ Proposed additional test
`@Test`
void parallelPhase_partialBudget_capsBatchToRemainingTurns() throws Exception {
    var engine = engine();
    when(memberTurnExecutor.executeAgentTurn(any(), any(), any(), any(), anyInt(), any(), any(), any(), any()))
            .thenAnswer(inv -> opinionEntry(((GroupMember) inv.getArgument(0)).agentId()));
    var speakers = List.of(member("a"), member("b"), member("c"));
    var gc = gc();
    var turnCounter = new AtomicInteger(0);

    engine.executeParallelPhase(gc, new AgentGroupConfiguration(), speakers, phase(TurnOrder.PARALLEL), protocol(), "Q?", 0, null,
            turnCounter, 2);

    assertEquals(2, gc.getTranscript().size());
    assertEquals(2, turnCounter.get());
    verify(memberTurnExecutor, times(2)).executeAgentTurn(any(), eq(gc), any(), any(), eq(0), any(), isNull(), isNull(), any());
}
🤖 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 130 - 141, Add a partial-budget test alongside
parallelPhase_zeroRemainingBudget_noop that invokes executeParallelPhase with
three speakers, turnCounter starting at 0, and maxTurns 2; stub
memberTurnExecutor to return entries, then assert exactly two transcript
entries, turnCounter equals 2, and the executor is called twice.
🤖 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/engine/internal/groups/PhaseExecutionEngine.java`:
- Around line 231-233: Update the comment immediately above the allMembers
stream in PhaseExecutionEngine to state that it collects and sorts all
configured group members, including the moderator; leave the implementation
unchanged unless the surrounding logic explicitly requires excluding moderators.

---

Nitpick comments:
In `@src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java`:
- Around line 254-262: Confirm that manual construction of GroupContextBuilder,
GroupSigningGuard, MemberTurnExecutor, and PhaseExecutionEngine is intentional,
and document the CDI migration as a follow-up in the plan document if it is
deferred. Keep the existing direct-construction behavior and unit-test
compatibility unchanged.

In
`@src/test/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngineTest.java`:
- Around line 130-141: Add a partial-budget test alongside
parallelPhase_zeroRemainingBudget_noop that invokes executeParallelPhase with
three speakers, turnCounter starting at 0, and maxTurns 2; stub
memberTurnExecutor to return entries, then assert exactly two transcript
entries, turnCounter equals 2, and the executor is called twice.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ae141f4-0e12-41ad-a4ed-6bc04134e2b7

📥 Commits

Reviewing files that changed from the base of the PR and between 741bc3f and 02d3760.

📒 Files selected for processing (4)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngineTest.java

Comment thread src/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/main/java/ai/labs/eddi/engine/internal/groups/GroupSigningGuard.java:93

  • signOutgoingMessage() assumes agentStore.getCurrentResourceId(agentId) is non-null and dereferences it immediately. When it is null (e.g., agent deleted / store returns no current version), this throws and is caught, but it logs a WARN and treats it like a signing failure. This is a normal “unsigned” condition and should short-circuit without exception/log noise; also response==null can be checked before hitting the store.
        try {
            var resourceId = agentStore.getCurrentResourceId(agentId);
            var agentConfig = agentStore.read(agentId, resourceId.getVersion());
            if (agentConfig.getSecurity() == null
                    || !agentConfig.getSecurity().isSignInterAgentMessages()
                    || response == null) {
                return SigningResult.UNSIGNED;
            }

src/main/java/ai/labs/eddi/engine/internal/groups/PhaseExecutionEngine.java:102

  • In executeParallelPhase(), maxTurns<=0 currently falls back to treating the budget as unlimited (remainingTurns = speakers.size()), which is inconsistent with executeSequentialPhase() / reserveTurn() semantics where maxTurns==0 means “no turns”. This can cause a parallel phase to run even when the caller intends a zero budget (and makes unit semantics inconsistent).
        // Cap batch size to remaining turn budget
        int remainingTurns = maxTurns > 0 ? Math.max(0, maxTurns - turnCounter.get()) : speakers.size();
        if (remainingTurns == 0) {
            return;
        }
        List<GroupMember> batchSpeakers = maxTurns > 0
                ? speakers.subList(0, Math.min(speakers.size(), remainingTurns))
                : speakers;

R1 step 6 of planning/group-collaboration-improvements-plan.md -- the
largest and most concurrency-sensitive extraction in Wave R: the entire
TASK_FORCE PLAN/EXECUTE/VERIFY cluster (~840 lines) into a new
TaskForceEngine. This is the code GroupConversationServiceConcurrencyTest
exists to pin -- the taskList -> transcript lock order, the
recordTaskFailure/notifyTaskFailure split, and resetStrandedInProgressTasks's
compare-and-set-under-monitor sweep all moved verbatim.

Did the bare-token reflection sweep first this time (the step-5 lesson).
Found 12 of 16 methods reflected -- more than any prior step -- including
several reached via a direct multi-line getDeclaredMethod(...) call that a
single-line pattern grep can't see. All 12 kept as delegators; the other 4
(executeTaskPlanPhase, abortWave, stringOrNull, notifyTaskFailure) had zero
matches anywhere and were fully inlined.

A 13th reflected method, reserveTurn, was hiding just outside the cluster's
own banner (in the prior "cooperative cancellation" section) and is
reflected as a static invocation (invoke(null, ...)). Missed by the
line-range-scoped search, caught before compiling by checking every method
actually called from the code being moved rather than trusting banner
boundaries. Moved to TaskForceEngine with a static delegator left behind,
same pattern as propagateDynamicAgentTracking in step 4.

Two bugs caught before reaching CI: a transcription error where I invented
non-existent ...ForTest-suffixed delegator method names instead of matching
TaskForceEngine's real ones (caught re-reading the diff, not by the
compiler); and a stale-TaskItem bug in the new TaskForceEngineTest itself
(TaskItem is an immutable record -- completeTask() returns a new instance
rather than mutating the one already held locally, so a test was checking
status==COMPLETED against a stale PENDING snapshot).

Full 12-class group suite + DynamicAgentTrackingPropagationTest + 6 focused
collaborator test classes green (555 tests), including
GroupConversationServiceConcurrencyTest (8/8) and
GroupConversationServiceTaskForceTest (20/20).

GroupConversationService: 3,432 -> 2,594 lines. 6 of R1's 10 steps done;
GroupConversationService is now smaller than both AgentOrchestrator (2,725)
and ConversationService (2,698).
Copilot AI review requested due to automatic review settings August 1, 2026 18:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/main/java/ai/labs/eddi/engine/internal/groups/GroupSigningGuard.java:129

  • signOutgoingMessage() can still persist a signed envelope even when no public key is available for the chosen keyVersion (publicKey == null). That skips the self-verification step described in the method’s contract, and produces transcript entries that peers cannot verify (verifyPriorEntriesIfRequired will log failures / cache null). Consider failing closed: if the key for the signed keyVersion cannot be resolved, drop the signature and return UNSIGNED (optionally with a warning log).
            String publicKey = agentConfig.getIdentity() != null
                    ? agentConfig.getIdentity().getKeyForVersion(keyVersion)
                    : null;
            if (publicKey != null) {
                boolean valid = agentSigningService.verifyEnvelope(signedEnvelope, publicKey);

docs/changelog.md:12

  • PR title/description say this PR covers R1 steps 1–3, but this diff includes R1 steps 4–6 as well (new MemberTurnExecutor, PhaseExecutionEngine, TaskForceEngine plus tests, and changelog entries for steps 4–6). Please either update the PR metadata to reflect the actual scope, or split steps 4–6 into a separate PR to match the stated review slice.

@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: 2

🧹 Nitpick comments (5)
src/test/java/ai/labs/eddi/engine/internal/groups/TaskForceEngineTest.java (3)

50-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the setup to @BeforeEach and close the executor.

engine() assigns the templatingEngine and jsonSerialization fields as a side effect of building the engine. Tests that call engine() inline and then assert on those fields depend on that ordering. tryParseVerificationJson_noBrackets_returnsFalseWithoutCallingDeserializer at Line 171 to Line 174 is the case: verifyNoInteractions(jsonSerialization) passes only because the field was reassigned during the inline engine() call.

engine() also creates a new Executors.newVirtualThreadPerTaskExecutor() per call and never closes it.

Build the mocks and the engine in a @BeforeEach method, and close the executor in @AfterEach.

♻️ Proposed test setup
     private ITemplatingEngine templatingEngine;
     private IJsonSerialization jsonSerialization;
+    private ExecutorService executorService;
+    private TaskForceEngine engine;
 
-    private TaskForceEngine engine() {
+    `@BeforeEach`
+    void setUp() {
         templatingEngine = mock(ITemplatingEngine.class);
         jsonSerialization = mock(IJsonSerialization.class);
-        return new TaskForceEngine(mock(MemberTurnExecutor.class), templatingEngine, jsonSerialization,
-                Executors.newVirtualThreadPerTaskExecutor(), new CallerIdentityContext(null, null),
-                new java.util.concurrent.ConcurrentHashMap<>(), 180, 5);
+        executorService = Executors.newVirtualThreadPerTaskExecutor();
+        engine = new TaskForceEngine(mock(MemberTurnExecutor.class), templatingEngine, jsonSerialization,
+                executorService, new CallerIdentityContext(null, null),
+                new ConcurrentHashMap<>(), 180, 5);
+    }
+
+    `@AfterEach`
+    void tearDown() {
+        executorService.close();
     }

Then replace each engine() call with the engine field.

🤖 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/TaskForceEngineTest.java`
around lines 50 - 56, Move creation of templatingEngine, jsonSerialization, the
virtual-thread executor, and TaskForceEngine from engine() into a `@BeforeEach`
setup method, storing the executor and engine in fields; replace every engine()
invocation with the initialized engine field. Add an `@AfterEach` method that
closes the executor to prevent per-test resource leaks.

201-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the reviewer-feedback append.

buildTaskExecutionInput appends task.verificationNote() to the prompt at Lines 746-748 of TaskForceEngine.java. That append drives the RETRY rejection policy, so a regression silently removes the reviewer feedback from the re-execution prompt. No test asserts it.

Add one case with a non-blank verificationNote.

💚 Proposed additional test
+    `@Test`
+    void buildTaskExecutionInput_withVerificationNote_appendsReviewerFeedback() throws Exception {
+        var gc = new GroupConversation();
+        gc.setId("gc-1");
+        var base = new TaskItem("Write the report", "cover Q1 results", 1);
+        var rejected = new TaskItem(base.id(), base.subject(), base.description(),
+                base.status(), base.assignedAgentId(), base.assignedDisplayName(),
+                base.dependsOnIds(), base.result(), "missing the revenue table",
+                false, base.priority(), base.createdAt(), base.completedAt());
+        var phase = new DiscussionPhase("EXEC", PhaseType.EXECUTE, "ALL", TurnOrder.SEQUENTIAL, ContextScope.NONE, false, null, 1, false);
+        var engine = engine();
+        when(templatingEngine.processTemplate(any(), any(), any())).thenReturn("Task prompt");
+
+        String input = engine.buildTaskExecutionInput(rejected, "Q?", phase, gc);
+
+        assertTrue(input.contains("missing the revenue table"));
+    }
🤖 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/TaskForceEngineTest.java`
around lines 201 - 215, Add a focused test alongside
buildTaskExecutionInput_templateFailure_fallsBackToPlainText that creates a
TaskItem with a non-blank verificationNote, invokes
TaskForceEngine.buildTaskExecutionInput, and asserts the returned prompt
contains that reviewer feedback. Keep the existing template-failure coverage
unchanged and verify the appended note is preserved in the re-execution input.

95-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the direct agentId branch.

resolveTaskAssignment has three branches: ALL or null, ROLE: prefix, and a direct agentId reference at Line 952 of TaskForceEngine.java. The direct agentId branch delegates to TaskListParser.resolveAgent and has no test here.

Add one case that passes an agentId as assignToRole.

💚 Proposed additional test
     `@Test`
     void resolveTaskAssignment_allWithNoEligibleMembers_fallsBackToFirstMember() {
         var members = List.of(member(MODERATOR));
         assertEquals(MODERATOR, engine().resolveTaskAssignment("ALL", members, MODERATOR, 0));
     }
+
+    `@Test`
+    void resolveTaskAssignment_directAgentIdReference_resolvesThatMember() {
+        var members = List.of(member(MODERATOR), member(AGENT_A), member(AGENT_B));
+        assertEquals(AGENT_B, engine().resolveTaskAssignment(AGENT_B, members, MODERATOR, 0));
+    }
🤖 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/TaskForceEngineTest.java`
around lines 95 - 99, Add a test in TaskForceEngineTest covering
resolveTaskAssignment’s direct agentId path: pass a concrete agentId as
assignToRole, provide matching members, and assert the returned assignment
resolves to that agent through TaskListParser.resolveAgent. Keep the existing
ALL and ROLE: branch tests unchanged.
src/main/java/ai/labs/eddi/engine/internal/groups/TaskForceEngine.java (1)

814-817: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the passed coercion into one helper.

Lines 814-817 and Lines 890-893 duplicate the same three-line coercion. tryParseVerificationJson uses it to set persisted task state. formatVerificationForDisplay uses it to render the pass or fail marker to the user. If one copy changes, the transcript shows a result that does not match the stored task status.

Extract a single private static helper and call it from both sites.

♻️ Proposed shared helper
-                    // Read 'passed' boolean directly from JSON
-                    boolean passed = true; // default to passed
-                    if (map.containsKey("passed")) {
-                        Object passedVal = map.get("passed");
-                        passed = Boolean.TRUE.equals(passedVal) || "true".equalsIgnoreCase(String.valueOf(passedVal));
-                    }
+                    boolean passed = readPassed(map);

Apply the same replacement at Lines 890-893, and add the helper next to stringOrNull:

    /**
     * Reads the {`@code` passed} flag from a deserialized verification item.
     * An absent key defaults to {`@code` true}.
     */
    private static boolean readPassed(Map<?, ?> map) {
        if (!map.containsKey("passed")) {
            return true;
        }
        Object passedVal = map.get("passed");
        return Boolean.TRUE.equals(passedVal) || "true".equalsIgnoreCase(String.valueOf(passedVal));
    }
🤖 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/TaskForceEngine.java`
around lines 814 - 817, Extract the duplicated passed-flag coercion from
tryParseVerificationJson and formatVerificationForDisplay into one private
static readPassed(Map<?, ?>) helper near stringOrNull. Preserve the existing
behavior: return true when "passed" is absent, otherwise accept Boolean.TRUE or
the case-insensitive string "true", and replace both inline implementations with
calls to the helper.
src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java (1)

256-266: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Consider CDI beans for the extracted collaborators.

GroupContextBuilder, GroupSigningGuard, MemberTurnExecutor, PhaseExecutionEngine, and TaskForceEngine are instantiated with new inside the constructor. The coding guidelines require Quarkus CDI for components. @ApplicationScoped beans plus @Inject would remove the manual wiring order constraints documented at Lines 256-261, and would keep the dependency graph visible to the container. The direct-construction unit tests can keep working through constructor injection on the beans themselves.

Note that this change also affects TaskForceEngine's access to activeTokens: a CDI-managed engine would need that map passed per call or owned by a shared holder rather than injected as constructor state.

As per coding guidelines: "Use Quarkus CDI (@ApplicationScoped and @Inject) for components; do not manually register modules."

🤖 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/GroupConversationService.java`
around lines 256 - 266, Convert GroupContextBuilder, GroupSigningGuard,
MemberTurnExecutor, PhaseExecutionEngine, and TaskForceEngine into CDI-managed
`@ApplicationScoped` components with constructor `@Inject` dependencies, and inject
them into GroupConversationService instead of constructing them with new. Remove
the constructor-order workaround comments and preserve direct constructor
usability for unit tests. Refactor TaskForceEngine so activeTokens is supplied
per invocation or through a shared holder rather than constructor state.

Source: Coding guidelines

🤖 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 `@docs/changelog.md`:
- Line 14: Update the reflected-method count in the changelog sentence to match
the listed methods: replace “two” with “four” while preserving the remaining
description and totals.

In `@src/main/java/ai/labs/eddi/engine/internal/groups/TaskForceEngine.java`:
- Around line 770-778: Update the fallback loop in parseAndApplyVerification to
re-fetch each task from gc.getTaskList() before checking its status or calling
verifyTask. Use the live task state to process only tasks still in COMPLETED
status, while preserving the existing verification event behavior for tasks that
are actually verified.

---

Nitpick comments:
In `@src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java`:
- Around line 256-266: Convert GroupContextBuilder, GroupSigningGuard,
MemberTurnExecutor, PhaseExecutionEngine, and TaskForceEngine into CDI-managed
`@ApplicationScoped` components with constructor `@Inject` dependencies, and inject
them into GroupConversationService instead of constructing them with new. Remove
the constructor-order workaround comments and preserve direct constructor
usability for unit tests. Refactor TaskForceEngine so activeTokens is supplied
per invocation or through a shared holder rather than constructor state.

In `@src/main/java/ai/labs/eddi/engine/internal/groups/TaskForceEngine.java`:
- Around line 814-817: Extract the duplicated passed-flag coercion from
tryParseVerificationJson and formatVerificationForDisplay into one private
static readPassed(Map<?, ?>) helper near stringOrNull. Preserve the existing
behavior: return true when "passed" is absent, otherwise accept Boolean.TRUE or
the case-insensitive string "true", and replace both inline implementations with
calls to the helper.

In `@src/test/java/ai/labs/eddi/engine/internal/groups/TaskForceEngineTest.java`:
- Around line 50-56: Move creation of templatingEngine, jsonSerialization, the
virtual-thread executor, and TaskForceEngine from engine() into a `@BeforeEach`
setup method, storing the executor and engine in fields; replace every engine()
invocation with the initialized engine field. Add an `@AfterEach` method that
closes the executor to prevent per-test resource leaks.
- Around line 201-215: Add a focused test alongside
buildTaskExecutionInput_templateFailure_fallsBackToPlainText that creates a
TaskItem with a non-blank verificationNote, invokes
TaskForceEngine.buildTaskExecutionInput, and asserts the returned prompt
contains that reviewer feedback. Keep the existing template-failure coverage
unchanged and verify the appended note is preserved in the re-execution input.
- Around line 95-99: Add a test in TaskForceEngineTest covering
resolveTaskAssignment’s direct agentId path: pass a concrete agentId as
assignToRole, provide matching members, and assert the returned assignment
resolves to that agent through TaskListParser.resolveAgent. Keep the existing
ALL and ROLE: branch tests unchanged.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 52f66241-d774-49f3-8f52-df2c3342cb3c

📥 Commits

Reviewing files that changed from the base of the PR and between 02d3760 and 7de245a.

📒 Files selected for processing (4)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/TaskForceEngine.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/TaskForceEngineTest.java

Comment thread docs/changelog.md Outdated
ginccc added 2 commits August 1, 2026 21:45
…teps 4-6

Independent review of R1 steps 4-6 (MemberTurnExecutor, PhaseExecutionEngine,
TaskForceEngine) before starting step 7. One stale class-Javadoc paragraph
fixed; one pre-existing concurrency-test-coverage gap flagged as a follow-up
task rather than fixed inline.
…Service

R1 step 7: unite the pause/cancel/timeout helpers and the cancelDiscussion/
resumeDiscussion public surface into one collaborator. Widens executeDiscussion,
resolvePhases and cleanupEphemeralAgents to public so the coordinator can call
back into the facade, same self-reference pattern as MemberTurnExecutor.

GroupConversationService: 2,594 -> 1,885 lines. 7 of R1's 10 steps done.
…er's tool

From PR review comments.

McpToolsProvider.discover added every spec to a list while writing executors
into a map, so two MCP servers exposing the same tool name left two specs and
only the LAST server's executor. ToolSourceRegistry then keeps the FIRST spec
and looks the executor up by name -- pairing server A's signature with server
B's implementation, so the model is shown one tool's contract while a
different tool runs. Now first-write-wins within the provider, a spec is only
added when its executor is present, and the collision is logged with the
toolsBlacklist remedy named.

Also removed a null guard of mine in addDynamicAgentTools that implied a
nullability the method does not honour three lines earlier.

Declined with reasoning: getRecruitedAgentIds "exposes internal state" --
RecruitAgentTool synchronizes on that list to make its cap check atomic and
it is a CopyOnWriteArrayList; a defensive copy would break the mutator.

Suite at baseline: 8 failures / 294 errors, all environmental.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 77 out of 107 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/main/java/ai/labs/eddi/engine/internal/groups/LiveDiscussionRegistry.java:93

  • There are two consecutive Javadoc blocks here, but only the second one is attached to a member. The first block (ending at line 93) is orphaned and will show up as stray documentation (and can trip Javadoc/checkstyle rules). Remove the first Javadoc block or merge its content into the following one.
    /**
     * The live instance for a running discussion, or empty if it is not currently
     * running (paused, finished, or never started on this node — group control is
     * per-node, like {@code activeTokens}). Callers resolve this via the
     * {@code groupConversationId} context var and must turn an empty result into an

ginccc added 2 commits August 4, 2026 14:41
The plan read as if none of it had been built. Marks Wave R, Wave 0,
Wave 1 (I1-I4) and I5/I7 as done, and records the known gaps in what
shipped so a follow-up branch starts from truth rather than from the
original intent -- notably that I1's ceiling cannot fire for an ordinary
group (non-cascade model calls price at zero), that decision_reached has
no producer, and that nested-group cost is overwritten rather than
accumulated.

Also records the two deliberate deviations from the plan's design (I3's
two-sided-roster + impartial-judge precondition, I5 assigning at file
time) so they read as decisions rather than drift.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 77 out of 107 changed files in this pull request and generated no new comments.

Suppressed comments (3)

src/main/java/ai/labs/eddi/engine/internal/groups/LiveDiscussionRegistry.java:126

  • get(String groupConversationId) currently has no Javadoc even though the class-level comments describe important semantics ("live only while running", callers should treat absence as non-running). Adding a short method-level Javadoc makes the contract easier to discover at the call sites.
    public Optional<GroupConversation> get(String groupConversationId) {

src/main/java/ai/labs/eddi/engine/internal/groups/LiveDiscussionRegistry.java:97

  • There is an orphaned/duplicated Javadoc block here (two /** ... */ blocks back-to-back), so the first block isn’t attached to any method and the combined comment is harder to read/maintain.

This issue also appears on line 126 of the same file.

    /**
     * The live instance for a running discussion, or empty if it is not currently
     * running (paused, finished, or never started on this node — group control is
     * per-node, like {@code activeTokens}). Callers resolve this via the
     * {@code groupConversationId} context var and must turn an empty result into an

src/main/java/ai/labs/eddi/configs/agents/model/AgentConfiguration.java:340

  • This return expression is correct but unnecessarily hard to read (versioned != null || version != 0 ? ...). Refactoring to an explicit early-return makes the intended fallback behavior (version 0 → publicKey, others → null) much clearer and reduces the risk of future mistakes.
            String versioned = keys.stream()
                    .filter(k -> k.version() == version)
                    .map(ai.labs.eddi.configs.agents.crypto.AgentPublicKey::publicKeyB64)
                    .findFirst()
                    .orElse(null);

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 77 out of 107 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/main/java/ai/labs/eddi/engine/internal/groups/LiveDiscussionRegistry.java:98

  • There are two adjacent Javadoc blocks here; the first one appears to describe get(...) but is currently attached to getForMember(...), leaving a redundant/incorrect doc comment. This is confusing for readers and can also trip Javadoc/checkstyle rules that expect exactly one Javadoc per member.
    /**
     * The live instance for a running discussion, or empty if it is not currently
     * running (paused, finished, or never started on this node — group control is
     * per-node, like {@code activeTokens}). Callers resolve this via the
     * {@code groupConversationId} context var and must turn an empty result into an

ginccc added 2 commits August 4, 2026 15:59
…next

The implementation plan is 500+ lines of design and carried no status, so
resuming meant re-deriving what had shipped from git history.

Adds planning/group-collaboration-NEXT.md as the sequencing authority and
collapses the status block added to the plan earlier today into a pointer
-- two files tracking status drift, and the stale one is the one that
gets read. The plan stays the design reference.

Records the two defects to fix before any new feature (unpriced ordinary
model calls, which make I1's ceiling unable to fire; I9 windowing), the
ordered queue with dependencies and the reasoning behind the order, the
off-critical-path gaps including the two whose obvious fixes were tried
and rejected, and the conventions that cost time on this branch.
Traced all three non-cascade paths rather than assuming: agent/tool loop,
legacy chat and streaming legacy chat each already emit tokenUsage, and
all three produce the exact keys computeCost reads. Confirms the fix is
config-field-only with no token-counting plumbing, and saves the next
session re-deriving it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 77 out of 108 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/main/java/ai/labs/eddi/engine/internal/groups/LiveDiscussionRegistry.java:94

  • There are two consecutive Javadoc blocks here; the first one (lines 87–93) is a dangling Javadoc that isn’t attached to any declaration. This is easy to miss in reviews and can trigger Javadoc/Checkstyle noise; it should be removed or merged with the next block.
    /**
     * The live instance for a running discussion, or empty if it is not currently
     * running (paused, finished, or never started on this node — group control is
     * per-node, like {@code activeTokens}). Callers resolve this via the
     * {@code groupConversationId} context var and must turn an empty result into an

Fifth review pass, aimed at the least-reviewed high-blast-radius code
rather than re-walking the group tools.

GroupConversation.schemaVersion defaults to CURRENT_SCHEMA_VERSION, so
the pre-F6 documents in production -- which have no such key -- load
claiming schema 3 while being version-1-shaped, and prepareForResume
loops zero times. The migration ladder never runs on the documents F6
exists to protect. Verified by deserialising a key-less document.

Zero impact today (MIGRATIONS is empty, no released build ever wrote a
version), so deliberately NOT fixed here: it cannot fire, and pushing it
would invalidate a green CI. Flagged do-it-before-#626-ships because the
fix is free only while no production document carries a version.

Filed with its two companions -- stale MIGRATIONS Javadoc, and a test
suite that never covers the key-less document, which is why this
survived four rounds. Also records three suspicions traced to ground and
cleared, so the next reviewer does not repeat the work.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 76 out of 108 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/main/java/ai/labs/eddi/engine/internal/groups/LiveDiscussionRegistry.java:93

  • There are two consecutive Javadoc blocks before getForMember(). The first one (describing get()) is redundant and isn’t attached to any method, which is easy to miss and can trip Javadoc/Checkstyle tooling. Remove the orphaned block (or merge its content into the get()/getForMember() docs).
    /**
     * The live instance for a running discussion, or empty if it is not currently
     * running (paused, finished, or never started on this node — group control is
     * per-node, like {@code activeTokens}). Callers resolve this via the
     * {@code groupConversationId} context var and must turn an empty result into an

src/main/java/ai/labs/eddi/engine/internal/groups/GroupConversationSchemaMigrations.java:33

  • GroupConversationSchemaMigrations’ Javadoc claims GroupConversation#CURRENT_SCHEMA_VERSION is 1 and “the first version that has ever existed”, but GroupConversation currently defines CURRENT_SCHEMA_VERSION as 3. This is misleading for future schema bumps (and for understanding why MIGRATIONS is empty). Update the comment to avoid hardcoding an incorrect value and instead state that no migrations are currently registered (identity fallback applies).
     * Migration functions, keyed by the version they upgrade <em>from</em> (so
     * entry {@code N} takes a version-{@code N} document to version {@code N+1}).
     * Empty today — {@link GroupConversation#CURRENT_SCHEMA_VERSION} is {@code 1},
     * the first version that has ever existed, so there is nothing yet to migrate
     * from. Every future Wave item that adds a resume-relevant field bumps

…e-split

# Conflicts:
#	src/main/java/ai/labs/eddi/engine/internal/ConversationService.java
#	src/main/java/ai/labs/eddi/modules/llm/impl/AgentOrchestrator.java
// this path (the 404 check above uses the cheaper getConversationState), and
// it is skipped entirely when toolDecisions is absent so the overwhelmingly
// common plain-verdict resume incurs no extra load.
if (decision != null && decision.getToolDecisions() != null && !decision.getToolDecisions().isEmpty()) {

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 76 out of 112 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/main/java/ai/labs/eddi/engine/internal/groups/LiveDiscussionRegistry.java:93

  • There are two Javadoc blocks back-to-back here; the first one (lines 87–93) is not attached to any declaration, which is confusing and can trip Javadoc/Checkstyle tooling. Either remove it or move it to document get(...).
    /**
     * The live instance for a running discussion, or empty if it is not currently
     * running (paused, finished, or never started on this node — group control is
     * per-node, like {@code activeTokens}). Callers resolve this via the
     * {@code groupConversationId} context var and must turn an empty result into an

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.

4 participants