Skip to content

fix(agents): null-version undeploy no-op, deploy under a CHM bin lock, EXECUTE wave deadline, protocol defaults - #648

Merged
ginccc merged 4 commits into
mainfrom
fix/agent-lifecycle-and-group-deadlines
Aug 10, 2026
Merged

fix(agents): null-version undeploy no-op, deploy under a CHM bin lock, EXECUTE wave deadline, protocol defaults#648
ginccc merged 4 commits into
mainfrom
fix/agent-lifecycle-and-group-deadlines

Conversation

@ginccc

@ginccc ginccc commented Aug 9, 2026

Copy link
Copy Markdown
Member

Wave A of a deep review of the Agent / Group Agent surface. The review's structural finding was that the group feature layer has been reviewed exhaustively while the agent lifecycle layer it stands on has not — four of these five fixes come from AgentFactory, two of them from a single six-line method.

1. undeployAgent(env, agentId, null) was a silent no-op — ephemeral agents leaked

AgentFactory.AgentId keys on (id, version), so new AgentId(id, null) equals no key the environment map ever holds. Both dynamic-agent teardown paths pass null:

  • GroupLifecycleOps#cleanupEphemeralAgents — runs after every group discussion
  • TeardownAgentTool — the LLM-facing teardown tool

So agentEnvironment.remove(...) and deployedAgents.remove(...) both did nothing, while the caller logged "undeployed agent '%s'" at INFO. agentStore.deleteAllPermanently(agentId) then ran anyway. Net result: the constructed agent stayed resolvable through getLatestReadyAgent after its configuration had been deleted from the store, and eddi_agents_deployed grew monotonically for the lifetime of the process.

A null version now means every deployed version of that agent — the only reading that matches what the teardown callers mean. The REST/admin path passes a real version and stays exact, which is why this was never noticed.

2. deployAgent ran store I/O inside ConcurrentHashMap.compute

compute holds the bin lock for the key while the mapping function runs, and the function's real work was agentStoreClientLibrary.getAgent() — a store read plus full workflow construction. ConcurrentHashMap documents that a mapping function must be short and must not touch other mappings of the same map; this held a bin lock across multi-second I/O, and any re-entrant agent resolution during construction would have deadlocked.

The claim is now a putIfAbsent of an IN_PROGRESS placeholder (atomic, no long hold) with the load outside the map. Side benefit: the placeholder is now actually published, which compute never did — the dummy was only ever returned on the failure path — so a concurrent getAgent() can observe "deployment in progress" instead of a bare null.

3. deployedAgents was mutated unsynchronized

Appended under synchronized (deployedAgents), removed with no lock at all, and read by the Micrometer gauge thread — three unordered accesses to a LinkedList. Now a CopyOnWriteArrayList, with addIfAbsent replacing a non-atomic contains()-then-add().

4. The TASK_FORCE EXECUTE wave gave up before its turns could have

The wave waited agentTimeoutSeconds × maxTasksPerAgent, which ignores retries — under onAgentFailure=RETRY a member legitimately gets timeout × (maxRetries + 1) — and carries no setup grace, so even a one-task no-retry wave could expire while the member was still inside its own budget (a member turn reaches its response wait only after agent lookup, conversation start and attachment grants).

That is exactly what PARALLEL_BATCH_GRACE_FLOOR_SECONDS exists to prevent, and both the parallel debate batch and the bid round in the same file already sized themselves through parallelBatchBudgetSeconds. The wave now does too, via a new extracted TaskForceEngine#waveBudgetSeconds so the derivation is assertable without timing a real wave.

5. The documented 180s agentTimeoutSeconds default was unreachable

resolveProtocol's fallback handed out a literal 60, and McpGroupTools.create_group hard-coded 60 — while the constant's own Javadoc, the four shipped templates and the published table in docs/group-conversations.md all said 180 (the value introduced because 60 timed out thinking models during synthesis). Since nothing backfills a protocol block at save time, a group saved without one — the common shape — ran at 60.

The defaults now live on ProtocolConfig (DEFAULT_AGENT_TIMEOUT_SECONDS, DEFAULT_MAX_RETRIES) as the single source of truth, referenced by the engine, the MCP tool and the follow-up path (resolveAgentTimeoutSeconds, which had its own stray return 60).

⚠️ Behaviour change, deliberate: groups with no protocol block, and MCP-created groups, go from a 60s to a 180s per-turn timeout. This makes the code agree with the documentation rather than the reverse.

Also

  • Removed an unreachable version comparison in getAllLatestAgents — it compared the result of getLatestAgent, which already returns the highest version, against itself.
  • Downgraded waitForDeploymentCompletion's "still IN_PROGRESS" ERROR to DEBUG when no deployment future was registered. With the placeholder now published that state is reachable and ordinary. Wiring the registration properly is a follow-up.

Testing

  • 820 existing tests green across AgentFactory*, AgentDeploymentManagement*, TaskForceEngine*, GroupConversationService*, McpGroupTools*, AgentGroupConfiguration*, RestAgentAdministration*.
  • 19 new tests in three classes.
  • The AgentFactory regression tests were mutation-checked: reverting the null-version branch fails 4 of 7.

Summary by CodeRabbit

  • Bug Fixes

    • Undeploying an agent without specifying a version now removes all deployed versions.
    • Agent deployment tracking is more reliable during concurrent deployments, failures, and redeployments.
    • Task-force execution waits now better account for retries, grace periods, and sequential tasks.
  • Improvements

    • Groups without protocol settings now use a 180-second agent timeout and two retry attempts.
    • Timeout and retry defaults are applied consistently across group creation and execution.
    • Improved lifecycle documentation and reduced unnecessary logging for routine in-progress states.

…, EXECUTE wave deadline, protocol defaults

Wave A of a deep review of the Agent / Group Agent surface. Four of these
five come from AgentFactory, two of them from a single six-line method.

1. undeployAgent(env, agentId, null) was a silent no-op. AgentId keys on
   (id, version), so new AgentId(id, null) equals no key the environment
   map ever holds. Both dynamic-agent teardown paths pass null
   (GroupLifecycleOps#cleanupEphemeralAgents after every group discussion,
   and TeardownAgentTool), so the removals did nothing while the caller
   logged success at INFO -- and deleteAllPermanently ran anyway. The
   constructed agent stayed resolvable via getLatestReadyAgent after its
   config had been deleted from the store, and eddi_agents_deployed grew
   monotonically for the process lifetime. A null version now means every
   deployed version, which is what the teardown callers mean; the
   REST/admin path passes a real version and stays exact.

2. deployAgent ran store I/O and full workflow construction inside
   ConcurrentHashMap.compute, holding a bin lock across multi-second I/O
   against the map's documented contract. The claim is now a putIfAbsent
   of an IN_PROGRESS placeholder with the load outside the map. The
   placeholder is now actually published (compute never did -- the dummy
   was only returned on failure), so a concurrent getAgent() can observe
   "in progress" instead of a bare null.

3. deployedAgents was appended under a synchronized block, removed with no
   lock at all, and read by the Micrometer gauge thread -- three unordered
   accesses to a LinkedList. Now CopyOnWriteArrayList with addIfAbsent.

4. The TASK_FORCE EXECUTE wave waited agentTimeoutSeconds x
   maxTasksPerAgent, which ignores retries (a RETRY member legitimately
   gets timeout x (maxRetries + 1)) and carries no setup grace. It now
   sizes itself through parallelBatchBudgetSeconds like the parallel
   debate batch and the bid round in the same file already did, via a new
   extracted TaskForceEngine#waveBudgetSeconds.

5. The documented 180s agentTimeoutSeconds default was unreachable:
   resolveProtocol's fallback and McpGroupTools.create_group both
   hard-coded 60, while the constant's Javadoc, the shipped templates and
   docs/group-conversations.md all said 180. Nothing backfills a protocol
   block at save time, so a group saved without one ran at 60. The
   defaults now live on ProtocolConfig as the single source of truth.

Behaviour change, deliberate: groups with no protocol block (and
MCP-created groups) go from a 60s to a 180s per-turn timeout, making the
code agree with the documentation rather than the reverse.

Also removes an unreachable version comparison in getAllLatestAgents and
downgrades waitForDeploymentCompletion's "still IN_PROGRESS" ERROR to
DEBUG when no deployment future was registered.

820 existing tests green, 19 new; the AgentFactory regression tests were
mutation-checked (4 of 7 fail against the old behaviour).
@ginccc
ginccc requested a review from rolandpickl as a code owner August 9, 2026 10:16
@github-actions

github-actions Bot commented Aug 9, 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 9, 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: 46 minutes

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: ec0a4714-dc69-41f0-b7be-f2cff637510b

📥 Commits

Reviewing files that changed from the base of the PR and between f1040af and 4f29b58.

📒 Files selected for processing (1)
  • docs/changelog.md
📝 Walkthrough

Walkthrough

The change centralizes protocol defaults, recalculates task-force wave budgets for retries and sequential tasks, and updates agent deployment tracking and null-version undeployment. New tests cover fallback resolution, wave budgets, deployment cleanup, gauges, and race handling.

Changes

Agent lifecycle and execution budgets

Layer / File(s) Summary
Shared protocol defaults
src/main/java/ai/labs/eddi/configs/groups/model/AgentGroupConfiguration.java, src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java, src/main/java/ai/labs/eddi/engine/mcp/McpGroupTools.java, src/test/java/ai/labs/eddi/engine/internal/GroupConversationServiceProtocolDefaultsTest.java
Defines shared 180-second timeout and two-attempt retry defaults. Group conversation and MCP group creation use these defaults. Tests cover fallback and configured protocol settings.
Task-force wave budgeting
src/main/java/ai/labs/eddi/engine/internal/groups/TaskForceEngine.java, src/test/java/ai/labs/eddi/engine/internal/groups/TaskForceEngineWaveBudgetTest.java
Wave waits use parallel batch budgets multiplied by the longest sequential task chain. Tests cover retries, grace periods, defaults, non-positive counts, and overflow safety.
Deployment and undeployment lifecycle
src/main/java/ai/labs/eddi/engine/runtime/IAgentFactory.java, src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java, src/test/java/ai/labs/eddi/engine/runtime/internal/AgentFactoryUndeployVersionTest.java, docs/changelog.md
Deployment uses atomic placeholders, loads agents outside map locks, conditionally publishes results, and derives gauges from READY agents. Null-version undeployment removes all versions. Tests validate cleanup, gauges, explicit-version behavior, and undeployment races.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentFactory
  participant ConcurrentHashMap
  participant AgentStore
  AgentFactory->>ConcurrentHashMap: Claim deployment with IN_PROGRESS placeholder
  AgentFactory->>AgentStore: Load agent outside map operation
  AgentStore-->>AgentFactory: Return loaded agent or failure
  AgentFactory->>ConcurrentHashMap: Publish READY agent or ERROR state
  AgentFactory->>ConcurrentHashMap: Remove all versions for null-version undeploy
Loading

Possibly related PRs

  • labsai/EDDI#626: Overlaps with the GroupConversationService timeout changes and TaskForceEngine wave-budget behavior.

Suggested reviewers: rolandpickl

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% 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 summarizes the main fixes for undeployment, deployment locking, EXECUTE wave deadlines, and protocol defaults.
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 fix/agent-lifecycle-and-group-deadlines

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

🤖 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/AgentGroupConfiguration.java`:
- Around line 943-955: Update the `agentTimeoutSeconds` record Javadoc to state
that its default is 180 seconds, matching `DEFAULT_AGENT_TIMEOUT_SECONDS`; leave
the constant and other documentation unchanged.

In `@src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java`:
- Around line 211-218: Synchronize the deployment completion transition in
AgentFactory with undeployAgent for the same AgentId so loading an agent cannot
restore it after teardown. Coordinate agentEnvironment replacement and
deployedAgents updates atomically; do not merely change put to replace. Apply
the same protection to the other deployment completion path around the
referenced logic, and add a regression test that blocks getAgent, undeploys,
then releases the lookup and verifies the agent remains undeployed.
🪄 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: 618afb1e-eba0-4d7e-b36e-a4e58ca34f63

📥 Commits

Reviewing files that changed from the base of the PR and between d5294a6 and 65c4fae.

📒 Files selected for processing (10)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/configs/groups/model/AgentGroupConfiguration.java
  • src/main/java/ai/labs/eddi/engine/internal/GroupConversationService.java
  • src/main/java/ai/labs/eddi/engine/internal/groups/TaskForceEngine.java
  • src/main/java/ai/labs/eddi/engine/mcp/McpGroupTools.java
  • src/main/java/ai/labs/eddi/engine/runtime/IAgentFactory.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java
  • src/test/java/ai/labs/eddi/engine/internal/GroupConversationServiceProtocolDefaultsTest.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/TaskForceEngineWaveBudgetTest.java
  • src/test/java/ai/labs/eddi/engine/runtime/internal/AgentFactoryUndeployVersionTest.java

Comment thread src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java Outdated
…tion, stale Javadoc

Addresses all four findings on #648.

CodeRabbit (Major) — and a genuine regression this PR introduced. Moving
the store load out of ConcurrentHashMap.compute opened a window in which
an undeployAgent for the same id could land mid-load, after which the
unconditional put of the finished agent resurrected it: a deployment
silently winning a race it started before the undeploy was even
requested, and leaving the metric describing a registry that no longer
held it. Publication is now replace(key, OUR placeholder, agent), so an
interleaved undeploy or competing redeploy keeps its outcome.

The reviewer's further point -- coordinate the map and the metric
together, do not only swap put for replace -- is addressed by deleting
the parallel structure entirely. deployedAgents is gone;
eddi_agents_deployed is now a Gauge derived from the environment maps
(READY entries only, matching the old semantics). Two structures holding
one fact could never be linearized against each other; one structure
needs no linearization.

CodeQL (4x log injection): the three claim-path log.debug(String.format(
...)) calls and the deploy-failure log.error interpolated an unsanitized
agentId. All now go through LogSanitizer.sanitize, and the debug calls
use debugf rather than pre-formatting.

CodeRabbit (Minor): ProtocolConfig's @PARAM agentTimeoutSeconds Javadoc
still said "default: 60".

Code-quality bot: a redundant assertion in TaskForceEngineWaveBudgetTest
(budget > 30 is implied by budget >= 90) replaced with
assertNotEquals(30L, budget), keeping the "not the old formula's output"
intent without the always-true comparison.

New test undeployDuringLoadIsNotOverwritten blocks the store lookup,
undeploys, then releases -- mutation-verified against a put. 111 tests
green across the AgentFactory/AgentDeploymentManagement suites.
@ginccc

ginccc commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Thanks — all four addressed in ba8f566.

@coderabbitai (Major) — deployment restoring an undeployed agent. Correct, and it was a regression this PR introduced: moving the store load out of ConcurrentHashMap.compute is exactly what opened the window. Publication is now replace(id, OUR placeholder, agent) so an interleaved undeploy (or a competing redeploy) keeps its outcome.

On your second point — "do not only replace put with replace; coordinate the map and metric tracking transition together" — I took the stronger option and deleted the parallel structure. deployedAgents is gone; eddi_agents_deployed is now a Gauge derived from the environment maps (READY entries only, matching the old semantics). Two structures holding one fact could never be linearized against each other; one structure needs no linearization. That also subsumes the unsynchronized-LinkedList fix this PR originally shipped.

Interleaving regression test added as requested (undeployDuringLoadIsNotOverwritten): it blocks the store lookup on a latch, undeploys, then releases, and asserts both the registry and the gauge. Mutation-verified — it fails against a put.

@github-advanced-security (CodeQL, 4× log injection). Fixed: the three claim-path log.debug(String.format(...)) calls and the deploy-failure log.error now route agentId through LogSanitizer.sanitize, and the debug calls use debugf instead of pre-formatting.

@coderabbitai (Minor) — stale Javadoc. Fixed: @param agentTimeoutSeconds now documents the 180s default and where the fallback applies.

@github-code-quality — useless comparison. Fixed as suggested: budget > 30 was implied by budget >= 90, replaced with assertNotEquals(30L, budget) so the "not the old formula's output" intent survives without an always-true condition.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline ba8f566
git show --format=fuller --find-renames --find-copies -- \
  src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java \
  src/test/java/ai/labs/eddi/engine/runtime/internal/AgentFactoryUndeployVersionTest.java \
  src/main/java/ai/labs/eddi/engine/runtime/IAgentFactory.java \
  src/main/java/ai/labs/eddi/engine/internal/groups/TaskForceEngine.java \
  ba8f566

printf '\n--- AgentFactory outline ---\n'
ast-grep outline src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java --items all --view expanded

printf '\n--- Focused deployment/metric/logging references ---\n'
rg -n -C 4 \
  'putIfAbsent|replace\(|deployedAgents|Gauge|LogSanitizer|debugf|undeployAgent|IN_PROGRESS|READY' \
  src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java

printf '\n--- Focused regression test ---\n'
rg -n -C 8 \
  'undeployDuringLoadIsNotOverwritten|CountDownLatch|gauge|MeterRegistry|undeployAgent' \
  src/test/java/ai/labs/eddi/engine/runtime/internal/AgentFactoryUndeployVersionTest.java

printf '\n--- Javadoc and wave-budget assertion ---\n'
rg -n -C 3 \
  '`@param` agentTimeoutSeconds|assertNotEquals\(30L' \
  src/main/java/ai/labs/eddi/engine/runtime/IAgentFactory.java \
  src/main/java/ai/labs/eddi/engine/internal/groups/TaskForceEngine.java \
  src/test/java/ai/labs/eddi/engine/internal/groups/TaskForceEngineWaveBudgetTest.java

Length of output: 40694


@ginccc Confirmed. Commit ba8f566 addresses the findings.

replace(id, placeholder, agent) prevents a completed store load from restoring an agent after undeployAgent removes the placeholder. The new undeployDuringLoadIsNotOverwritten test covers this interleaving and verifies both the registry and eddi_agents_deployed.

The derived Gauge removes the separate deployedAgents state. It counts only READY entries in the environment maps. This keeps the metric consistent with the runtime registry.

The claim-path logs now sanitize agentId and use debugf. The Javadoc and wave-budget assertion updates are also present.

🐇 ✅


✏️ Learnings added
Learnt from: ginccc
URL: https://github.com/labsai/EDDI/pull/648

Timestamp: 2026-08-09T10:46:31.199Z
Learning: In `src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java`, `eddi_agents_deployed` must derive its count from READY entries in the environment maps. Do not maintain a separate deployed-agent collection because it cannot be kept linearizable with concurrent deploy and undeploy operations.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

…ure; honest gauge semantics

Two findings from a model-independent second pass over the full diff.

1. waitForDeploymentCompletion armed orTimeout(60s) on the SHARED
   registration future. orTimeout mutates the future it is called on, and
   DeploymentListener hands the same instance to every waiter and to
   whoever registered the deployment. Pre-#651 only the ZIP importer ever
   held one, so the mutation was near-unreachable; once #651 registers on
   every REST deploy, one impatient getAgent caller would -- at its own
   60s deadline -- complete the shared future exceptionally for every
   other consumer (failing waiters whose deployment was still legitimately
   in flight) and evict the registration from the listener's map before
   the real deployment event arrived. Now a timed get(), which waits
   without writing. The newly-reachable InterruptedException restores the
   interrupt flag, and a timeout logs at WARN as this caller's outcome
   rather than ERROR as the deployment's.

2. The gauge comment claimed "matching the old semantics"; one delta is
   real and deliberate. The old deployedAgents list keyed on (id, version)
   with no environment, so an agent deployed to both production and test
   counted once; counting map entries counts it per environment -- per
   actual deployment. The truthful reading, now stated in the comment and
   the changelog instead of implied away.

110 tests green across AgentFactory*/DeploymentListener*/
AgentDeploymentManagement*.

@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

Caution

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

⚠️ Outside diff range comments (2)
src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java (2)

307-312: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not remove a replacement deployment from a stale snapshot.

Line 311 removes each snapshotted key without checking its mapped value. If another caller removes and redeploys one version after the snapshot, this loop can remove the newer placeholder or READY agent.

Capture each matching key and value, then use remove(key, capturedValue). Add an interleaving test for a null-version undeploy racing with a redeploy of one captured version.

Proposed fix
- List<AgentId> allVersions = agentEnvironment.keySet().stream()
-         .filter(key -> Objects.equals(key.getId(), agentId))
+ List<Map.Entry<AgentId, IAgent>> allVersions = agentEnvironment.entrySet().stream()
+         .filter(entry -> Objects.equals(entry.getKey().getId(), agentId))
+         .map(entry -> Map.entry(entry.getKey(), entry.getValue()))
          .toList();
- allVersions.forEach(agentEnvironment::remove);
+ allVersions.forEach(entry -> agentEnvironment.remove(entry.getKey(), entry.getValue()));
🤖 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/runtime/internal/AgentFactory.java` around
lines 307 - 312, Update the null-version branch in AgentFactory’s undeploy logic
to snapshot each matching key together with its mapped value, then remove
entries conditionally via remove(key, capturedValue) so redeployed replacements
are preserved. Add an interleaving test covering null-version undeploy racing
with redeployment of one captured version.

150-196: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Remove the synchronous deployment wait.

Line 164 can block a backend caller for 60 seconds. This can exhaust request or worker threads during concurrent deployments.

Return a not-ready result immediately, or propagate deployment completion through a non-blocking API.

As per coding guidelines, backend code must be thread-safe and non-blocking; use AsyncResponse for REST endpoints and avoid extended blocking in tasks.

🤖 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/runtime/internal/AgentFactory.java` around
lines 150 - 196, Remove the blocking deploymentFuture.get(...) call from
waitForDeploymentCompletion. Return the existing not-ready result immediately
when deploymentFuture indicates an in-progress deployment, or propagate
completion through a non-blocking API; preserve the agent lookup and status
handling without synchronously waiting up to DEPLOYMENT_WAIT_SECONDS.

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`:
- Around line 23-24: Update the two changelog bullets in the deployment-wait and
gauge descriptions: complete the sentence describing waitForDeploymentCompletion
so it directly states that it uses a timed get() without mutating the shared
future, and revise the gauge wording to state clearly that READY deployments are
counted per environment.

---

Outside diff comments:
In `@src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java`:
- Around line 307-312: Update the null-version branch in AgentFactory’s undeploy
logic to snapshot each matching key together with its mapped value, then remove
entries conditionally via remove(key, capturedValue) so redeployed replacements
are preserved. Add an interleaving test covering null-version undeploy racing
with redeployment of one captured version.
- Around line 150-196: Remove the blocking deploymentFuture.get(...) call from
waitForDeploymentCompletion. Return the existing not-ready result immediately
when deploymentFuture indicates an in-progress deployment, or propagate
completion through a non-blocking API; preserve the agent lookup and status
handling without synchronously waiting up to DEPLOYMENT_WAIT_SECONDS.
🪄 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: aad1d516-2b4a-4501-a7bc-84aab58cf66c

📥 Commits

Reviewing files that changed from the base of the PR and between 65c4fae and f1040af.

📒 Files selected for processing (5)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/configs/groups/model/AgentGroupConfiguration.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/AgentFactory.java
  • src/test/java/ai/labs/eddi/engine/internal/groups/TaskForceEngineWaveBudgetTest.java
  • src/test/java/ai/labs/eddi/engine/runtime/internal/AgentFactoryUndeployVersionTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/ai/labs/eddi/engine/internal/groups/TaskForceEngineWaveBudgetTest.java
  • src/main/java/ai/labs/eddi/configs/groups/model/AgentGroupConfiguration.java

Comment thread docs/changelog.md Outdated
@ginccc

ginccc commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Final review pass (independent second-model review of the full diff) — two further items, fixed in the latest commit:

  1. waitForDeploymentCompletion armed orTimeout(60s) on the shared registration future. orTimeout mutates the future it's called on, and DeploymentListener hands the same instance to every waiter and to whoever registered the deployment. Pre-fix(agents): wire the deployment-wait machinery that only the ZIP importer ever used #651 the mutation was near-unreachable (only the ZIP importer ever registered); once fix(agents): wire the deployment-wait machinery that only the ZIP importer ever used #651 registers on every REST deploy, one impatient getAgent caller would — at its own 60s deadline — complete the shared future exceptionally for every other consumer and evict the registration before the real deployment event arrived. Now a timed get(), which waits without writing. The newly-reachable InterruptedException restores the interrupt flag, and a timeout logs at WARN as this caller's outcome rather than ERROR as the deployment's.
  2. Honest gauge semantics. The gauge comment claimed "matching the old semantics", but one delta is real: the old deployedAgents list keyed on (id, version) with no environment, so an agent deployed to both production and test counted once; counting map entries counts it per environment — per actual deployment. Deliberate and the truthful reading; now stated in the comment and changelog instead of implied away.

The pass also re-verified the deploy/undeploy CAS, the null-version undeploy, and the wave-budget derivation against the live code paths — no further findings.

@ginccc

ginccc commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Fixed the changelog wording nitpick in the latest commit — both bullets now lead with the behavior ("waits with a timed get() and no longer mutates the shared registration future"; "counts READY deployments per environment") and the sentence fragment is gone.

@ginccc
ginccc merged commit 9573e07 into main Aug 10, 2026
24 checks passed
pull Bot pushed a commit to Stars1233/EDDI that referenced this pull request Aug 10, 2026
…not yet supported" section

Wave E of the Agent / Group Agent review -- the documentation drifts the
review turned up, each one a place where docs and code disagreed.

1. Attachments x groups were entirely undocumented. group-conversations.md
   had zero mentions of attachments and attachments-guide.md zero mentions
   of groups, while POST /groups/{groupId}/conversations accepts them and
   GroupAttachmentBinder is a whole subsystem. Both files now cover the
   three input shapes, the first-turn grant, how later phases keep access
   (history plus the auto-enabled readAttachment tool), and the two
   group-specific bounds: the per-turn cap applies per MEMBER turn, and
   anything dropped is reported in that member's attachments:errors, not
   in the group transcript.

2. The protocol table did not say the defaults apply to an absent block.
   Nothing backfills a stored config, so "no protocol block" is the common
   shape -- which is what made the 60-vs-180 drift fixed in labsai#648
   invisible.

3. maxCreatedAgentsPerDiscussion now reads "counted across all members,
   not per member" -- the behaviour labsai#649 delivers.

4. LAST_PHASE was documented as "only the previous phase's entries", but
   the filter is phaseIndex >= currentPhaseIdx - 1, which includes the
   running phase. The code is right -- in a sequential phase that is what
   lets the second speaker react to the first -- so the doc and the enum
   Javadoc were corrected, not the filter.

5. New "Not yet supported" section: member-level tool approval inside a
   group, nested pauses, groups over the OpenAI-compatible /v1 adapter,
   groups over A2A, and the per-node scope of the live-discussion
   registry.

Also: an FQN sweep of CreateSubAgentTool (11 inline fully-qualified names,
against AGENTS.md 4.7) and the orphaned Javadoc in LiveDiscussionRegistry,
where the paragraph documenting get() sat above getForMember() so both
attached to the latter and get() had none.

Scoping: the FQN violation is repo-wide (~130 sites). This sweeps only
files no other open PR touches; the rest is a follow-up once labsai#648-labsai#651
land.
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