Skip to content

fix(groups): a paused cadence discussion no longer wedges a standing team forever - #650

Merged
ginccc merged 2 commits into
mainfrom
fix/cadence-claim-expiry
Aug 10, 2026
Merged

fix(groups): a paused cadence discussion no longer wedges a standing team forever#650
ginccc merged 2 commits into
mainfrom
fix/cadence-claim-expiry

Conversation

@ginccc

@ginccc ginccc commented Aug 9, 2026

Copy link
Copy Markdown
Member

Wave C of the Agent / Group Agent review (Wave A #648, Wave B #649) — a liveness defect in I13 Standing Teams, the newest part of the group subsystem.

The defect

TeamCadenceService.reconcile releases a workspace's runningDiscussionId claim when its discussion reaches a terminal state:

case COMPLETED -> writebackCompleted(workspace, gc);
case FAILED, CANCELLED -> writebackFailure(workspace, gc);
default -> false; // IN_PROGRESS, SYNTHESIZING, AWAITING_* — still running

AWAITING_APPROVAL and AWAITING_HUMAN_INPUT fall into default — correctly, for a discussion that will be approved. But the default group HITL timeout policy is WAIT_INDEFINITELY (AgentGroupConfiguration:1354), so a pause nobody resolves never becomes terminal either.

The claim was therefore held forever:

  • every subsequent cadence fire for that group is skipped as "Previous cadence discussion … is still running",
  • the backlog tasks that run pulled stay IN_PROGRESS,
  • and there is no claim TTL and no reaper anywhere.

One unapproved run permanently wedges a standing team, with no unusual configuration required. That is out of character for this subsystem — its task-force half carries an explicit no-progress fingerprint guard (taskPauseFingerprint) precisely to guarantee termination.

The fix

  • GroupWorkspace.claimedAt — a nullable stamp written next to runningDiscussionId and cleared next to it in settle(). The two are one fact, so they are set and cleared together. Nullable on purpose: documents written before the field existed have no stamp, and reclaiming those on a missing timestamp would be a guess — they get one on their next claim.
  • reclaimIfStalereconcile's non-terminal arm now routes through it. It cancels the stranded discussion, then runs the ordinary failure writeback (pulled tasks back to PENDING, claim cleared) rather than a bespoke path. Cancel before release, so a reclaimed run cannot keep spending against a budget nobody is tracking any more.
  • eddi.groups.cadence.claim-ttl, default PT24H. Deliberately generous: an approval that arrives the next business morning must still land on the discussion it belongs to, not on a reclaimed corpse. This is a liveness backstop for a wedged team, not an SLA. Non-positive disables reclaiming entirely, for an operator who would rather wedge than risk abandoning a pause.
  • eddi_team_cadence_claims_reclaimed_total, plus a WARN naming the stranded discussion, its state, and how long the claim was held.
  • A save-time warning when adding a cadence to a group that combines requiresApproval phases with WAIT_INDEFINITELY — that combination is exactly what makes the backstop reachable, and the operator almost certainly wanted a finite timeoutPolicy (which resolves such pauses properly instead of abandoning them). A warning, not a rejection: the combination is legitimate for a team whose approver genuinely is always available, and rejecting it would break existing configs.

Also

cancelQuietly now passes ControlSignal.CANCEL_GRACEFUL explicitly. null already resolved to graceful — only CANCEL_IMMEDIATE takes the other branch in GroupHitlCoordinator — but the method has a second caller now, and "which cancel is this?" should not require reading another class to answer.

Testing

585 tests green across TeamCadence*, GroupWorkspace*, RestGroupWorkspace*, GroupConversationService*; 9 new in TeamCadenceClaimExpiryTest, covering both pause states, a fresh pause inside the TTL (must keep its claim), a genuinely running discussion, a missing stamp, a disabled TTL, an idle workspace, and that the ordinary COMPLETED path is not shadowed.

Summary by CodeRabbit

  • New Features

    • Added automatic recovery for paused group discussions whose cadence claims exceed a configurable time limit.
    • Reclaimed work is returned to pending status and recorded in metrics.
    • Added warnings for approval stages configured with indefinite human-input waits.
  • Bug Fixes

    • Improved claim tracking and cleanup when discussions are completed or cancelled.
  • Documentation

    • Documented cadence claim expiry behavior in the changelog.

…team forever

Wave C of the Agent / Group Agent review -- a liveness defect in I13
Standing Teams.

TeamCadenceService.reconcile releases a workspace's runningDiscussionId
claim when its discussion reaches a terminal state. AWAITING_APPROVAL and
AWAITING_HUMAN_INPUT are not terminal, so they fell into the
"default -> false" (still running) arm -- correctly, for a discussion that
will be approved. But the default group HITL timeout policy is
WAIT_INDEFINITELY, so a pause nobody resolves never becomes terminal
either, and the claim was held forever: every subsequent cadence fire for
that group was skipped as "still running", and the backlog tasks that run
had pulled stayed IN_PROGRESS. There was no claim TTL and no reaper
anywhere -- out of character for a subsystem whose task-force half carries
an explicit no-progress guard precisely to guarantee termination.

- GroupWorkspace gains a nullable claimedAt stamp, written next to
  runningDiscussionId and cleared next to it in settle(). Nullable on
  purpose: documents predating the field have no stamp, and reclaiming
  those on a missing timestamp would be a guess.
- reconcile's non-terminal arm routes through reclaimIfStale, which
  cancels the stranded discussion and runs the ORDINARY failure writeback
  (pulled tasks back to PENDING, claim cleared) rather than a bespoke
  path. Cancel before release, so a reclaimed run cannot keep spending
  against a budget nobody tracks.
- TTL is eddi.groups.cadence.claim-ttl, default PT24H. Deliberately
  generous: an approval arriving the next business morning must land on
  the discussion it belongs to, not on a reclaimed corpse. Non-positive
  disables reclaiming.
- New counter eddi_team_cadence_claims_reclaimed_total, plus a WARN naming
  the stranded discussion and how long the claim was held.
- Adding a cadence now warns when the group combines requiresApproval
  phases with WAIT_INDEFINITELY -- that combination is what makes the
  backstop reachable. A warning, not a rejection: it is legitimate for a
  team whose approver really is always available.

Also: cancelQuietly now passes CANCEL_GRACEFUL explicitly. null already
resolved to graceful, but the method has a second caller now.

585 tests green; 9 new.
@ginccc
ginccc requested a review from rolandpickl as a code owner August 9, 2026 10:56
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Cadence claims now store acquisition timestamps. Configurable TTL handling reclaims stale paused discussions through graceful cancellation and normal task writeback. Cadence creation warns when approval phases can wait indefinitely for human input.

Changes

Cadence claim lifecycle

Layer / File(s) Summary
Persist cadence claim state
src/main/java/ai/labs/eddi/configs/groups/model/GroupWorkspace.java, src/main/java/ai/labs/eddi/engine/runtime/internal/TeamCadenceService.java
GroupWorkspace stores nullable claimedAt. TeamCadenceService records claim times and injects a configurable TTL with a 24-hour default.
Reclaim expired claims
src/main/java/ai/labs/eddi/engine/runtime/internal/TeamCadenceService.java, src/test/java/ai/labs/eddi/engine/runtime/internal/TeamCadenceClaimExpiryTest.java, src/test/java/ai/labs/eddi/engine/runtime/internal/TeamCadenceServiceTest.java, docs/changelog.md
Reconciliation reclaims stale non-terminal claims, cancels discussions with CANCEL_GRACEFUL, returns tasks to PENDING, records metrics, and clears timestamps. Tests cover TTL and discussion-state behavior.
Warn about indefinite approval pauses
src/main/java/ai/labs/eddi/configs/groups/rest/RestGroupWorkspace.java
Cadence creation logs a warning when approval phases use missing, null, or WAIT_INDEFINITELY HITL timeouts. Creation remains allowed.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TeamCadenceService
  participant GroupWorkspaceStore
  participant GroupConversationService
  TeamCadenceService->>GroupWorkspaceStore: Read claim timestamp and running discussion
  TeamCadenceService->>GroupConversationService: Cancel expired discussion gracefully
  TeamCadenceService->>GroupWorkspaceStore: Return tasks to PENDING and release claim
Loading

Possibly related PRs

  • labsai/EDDI#640: Introduced the AWAITING_HUMAN_INPUT state that this PR includes in claim expiry handling.
  • labsai/EDDI#644: Added related cadence-claim logic extended here with timestamps, TTL reclamation, and graceful cancellation.

Suggested reviewers: rolandpickl

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 primary fix: stale paused cadence discussions no longer retain a standing team indefinitely.
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/cadence-claim-expiry

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.

@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 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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/rest/RestGroupWorkspace.java`:
- Around line 331-333: Update the warning construction in RestGroupWorkspace so
it does not unconditionally claim the claim TTL will reclaim the run. Make the
wording conditional on a positive, enabled eddi.groups.cadence.claim-ttl value,
and explicitly indicate that non-positive values can retain runningDiscussionId
indefinitely.
- Around line 193-197: Move the indefinite-pause warning block using
indefinitePauseWarning and LOG.warnf to after scheduleStore.createSchedule and
workspaceStore.update both complete successfully, so it only reports a persisted
cadence. Keep the existing warning message and sanitization unchanged.

In `@src/main/java/ai/labs/eddi/engine/runtime/internal/TeamCadenceService.java`:
- Around line 339-343: Update the cancellation handling in the flow around
cancelQuietly and writebackFailure so failure writeback occurs only after
cancellation is confirmed; when cancellation fails or is unconfirmed, retain the
claim or re-read the discussion and follow the normal terminal-state path. Move
cadenceClaimsReclaimed.increment() to execute only when
writebackFailure(workspace, gc) returns true, preventing failed CAS races from
being counted as reclaimed claims.
🪄 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: 872b92cd-488f-43fe-b797-4c846c635988

📥 Commits

Reviewing files that changed from the base of the PR and between 49ef428 and d8a2d86.

📒 Files selected for processing (6)
  • docs/changelog.md
  • src/main/java/ai/labs/eddi/configs/groups/model/GroupWorkspace.java
  • src/main/java/ai/labs/eddi/configs/groups/rest/RestGroupWorkspace.java
  • src/main/java/ai/labs/eddi/engine/runtime/internal/TeamCadenceService.java
  • src/test/java/ai/labs/eddi/engine/runtime/internal/TeamCadenceClaimExpiryTest.java
  • src/test/java/ai/labs/eddi/engine/runtime/internal/TeamCadenceServiceTest.java

Comment on lines +193 to +197
String indefinitePause = indefinitePauseWarning(groupId);
if (indefinitePause != null) {
LOG.warnf("Cadence added to group %s which pauses for approval under WAIT_INDEFINITELY: %s",
sanitize(groupId), indefinitePause);
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Log the warning only after cadence persistence succeeds.

This block runs before scheduleStore.createSchedule and workspaceStore.update. If either operation fails, the log states that a cadence was added even though no cadence was persisted. Move the warning after the successful workspace update, or use preflight wording.

Proposed fix
 String indefinitePause = indefinitePauseWarning(groupId);
-if (indefinitePause != null) {
-    LOG.warnf("Cadence added to group %s which pauses for approval under WAIT_INDEFINITELY: %s",
-            sanitize(groupId), indefinitePause);
-}
 ...
 workspaceStore.update(workspace);
+if (indefinitePause != null) {
+    LOG.warnf("Cadence added to group %s which pauses for approval under WAIT_INDEFINITELY: %s",
+            sanitize(groupId), indefinitePause);
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/configs/groups/rest/RestGroupWorkspace.java`
around lines 193 - 197, Move the indefinite-pause warning block using
indefinitePauseWarning and LOG.warnf to after scheduleStore.createSchedule and
workspaceStore.update both complete successfully, so it only reports a persisted
cadence. Keep the existing warning message and sanitization unchanged.

Comment on lines +331 to +333
return "the group has requiresApproval phase(s) and hitlConfig.timeoutPolicy=WAIT_INDEFINITELY, so an "
+ "unapproved run holds this team's cadence claim until the claim TTL reclaims it; set a finite "
+ "timeoutPolicy to resolve such pauses properly";

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Make the warning accurate when claim reclamation is disabled.

Non-positive eddi.groups.cadence.claim-ttl values disable reclamation. This message always says that the claim TTL reclaims the run. In the disabled configuration, an unapproved discussion can retain runningDiscussionId indefinitely.

Make the message conditional on an enabled positive TTL, or remove the unconditional reclamation promise.

Proposed wording
-                    + "unapproved run holds this team's cadence claim until the claim TTL reclaims it; set a finite "
-                    + "timeoutPolicy to resolve such pauses properly";
+                    + "unapproved run can hold this team's cadence claim indefinitely; set a finite timeoutPolicy "
+                    + "and configure a positive eddi.groups.cadence.claim-ttl if reclamation is desired";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return "the group has requiresApproval phase(s) and hitlConfig.timeoutPolicy=WAIT_INDEFINITELY, so an "
+ "unapproved run holds this team's cadence claim until the claim TTL reclaims it; set a finite "
+ "timeoutPolicy to resolve such pauses properly";
return "the group has requiresApproval phase(s) and hitlConfig.timeoutPolicy=WAIT_INDEFINITELY, so an "
"unapproved run can hold this team's cadence claim indefinitely; set a finite timeoutPolicy "
"and configure a positive eddi.groups.cadence.claim-ttl if reclamation is desired";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/ai/labs/eddi/configs/groups/rest/RestGroupWorkspace.java`
around lines 331 - 333, Update the warning construction in RestGroupWorkspace so
it does not unconditionally claim the claim TTL will reclaim the run. Make the
wording conditional on a positive, enabled eddi.groups.cadence.claim-ttl value,
and explicitly indicate that non-positive values can retain runningDiscussionId
indefinitely.

Comment on lines +339 to +343
cancelQuietly(gc.getId());
cadenceClaimsReclaimed.increment();
// The ordinary failure writeback: returns the pulled tasks to PENDING and
// clears the claim, exactly as a FAILED/CANCELLED discussion would.
return writebackFailure(workspace, gc);

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not release a claim when cancellation is unconfirmed.

Line 339 calls cancelQuietly, but Lines 517-526 discard both exceptions and the boolean result from cancelDiscussion. Line 343 then releases the claim and returns tasks to PENDING even if cancellation failed. The original discussion can continue without a workspace claim while a later fire starts another discussion.

Only run failure writeback after cancellation succeeds. If cancellation fails, keep the claim or re-read the discussion and use the normal terminal-state path. Increment cadenceClaimsReclaimed only after writebackFailure returns true, or failed CAS races overcount reclaimed claims.

Proposed fix
-        cancelQuietly(gc.getId());
-        cadenceClaimsReclaimed.increment();
-        // The ordinary failure writeback: returns the pulled tasks to PENDING and
-        // clears the claim, exactly as a FAILED/CANCELLED discussion would.
-        return writebackFailure(workspace, gc);
+        if (!cancelQuietly(gc.getId())) {
+            return false;
+        }
+        // The ordinary failure writeback: returns the pulled tasks to PENDING and
+        // clears the claim, exactly as a FAILED/CANCELLED discussion would.
+        boolean released = writebackFailure(workspace, gc);
+        if (released) {
+            cadenceClaimsReclaimed.increment();
+        }
+        return released;
-private void cancelQuietly(String discussionId) {
+private boolean cancelQuietly(String discussionId) {
     try {
-        groupConversationService.cancelDiscussion(discussionId, ControlSignal.CANCEL_GRACEFUL);
+        return groupConversationService.cancelDiscussion(discussionId, ControlSignal.CANCEL_GRACEFUL);
     } catch (Exception e) {
         LOGGER.warnf("Could not cancel discussion %s after a lost or expired cadence claim: %s", discussionId, e.getMessage());
+        return false;
     }
 }

Also applies to: 517-526

🤖 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/TeamCadenceService.java`
around lines 339 - 343, Update the cancellation handling in the flow around
cancelQuietly and writebackFailure so failure writeback occurs only after
cancellation is confirmed; when cancellation fails or is unconfirmed, retain the
claim or re-read the discussion and follow the normal terminal-state path. Move
cadenceClaimsReclaimed.increment() to execute only when
writebackFailure(workspace, gc) returns true, preventing failed CAS races from
being counted as reclaimed claims.

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.

2 participants