Repository navigation
feat: add Loop lifecycle runtime foundations (stack 1/3) - #300
Conversation
Checkpoint via cy-loop-tasks (iteration 2, phase B mode=tasks). Co-Authored-By: Codex <[email protected]>
Checkpoint via cy-loop-tasks (iteration 4, phase B mode=tasks). Co-Authored-By: Codex <[email protected]>
Checkpoint via cy-loop-tasks (iteration 5, phase B mode=tasks). Co-Authored-By: Codex <[email protected]>
|
Too many files changed for review. ( Bypass the limit by tagging |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (5)
WalkthroughThis change adds loop lifecycle configuration, retry and quarantine execution, durable attempts and controls, effect outbox delivery, target health management, retry recovery, lifecycle DSL validation, and database migrations. ChangesLoop lifecycle configuration
Coordinator execution and daemon runtime
Durable storage and scheduling
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (15)
internal/loop/effects_dispatch_test.go-119-239 (1)
119-239: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winWrap both scenarios in
t.Run("Should …")subtests.This test declares no subtest. It runs two independent scenarios in one function body: node and terminal trigger attachment at lines 122-190, and quarantine attachment at lines 192-238. A
t.Fatalfin the first scenario stops the run and hides a defect in the second one.Split the body into two subtests, for example "Should attach node lifecycle and terminal triggers" and "Should attach quarantine context to an on_quarantine effect". Keep the shared
failureClassandnextAttemptAtvalues in the outer function.As per path instructions: "MUST use t.Run("Should...") pattern for ALL test cases".
🤖 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 `@internal/loop/effects_dispatch_test.go` around lines 119 - 239, Split TestCoordinatorEffectsShouldAttachNodeAndTerminalTriggers into two t.Run subtests using names beginning with “Should”: one for node lifecycle and terminal trigger attachment, and one for quarantine context attachment. Keep shared failureClass and nextAttemptAt declarations in the outer test, and move each independent scenario’s setup, execution, and assertions into its corresponding subtest.Source: Path instructions
internal/loop/effects_dispatch_test.go-247-263 (1)
247-263: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdd the canceled terminal outcome to this coverage test.
dsl.TerminalCanceled,dsl.TerminalEffects.OnCanceled, andEffectTriggerOnCanceledexist, butTestCoordinatorEffectsShouldAttachEveryDeclaredTerminalOutcomestill only covers the six earlier terminal statuses. Add thecanceledcase so the boundary mapping and test name are accurate.🤖 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 `@internal/loop/effects_dispatch_test.go` around lines 247 - 263, Add the canceled terminal outcome to TestCoordinatorEffectsShouldAttachEveryDeclaredTerminalOutcome by configuring dsl.TerminalEffects.OnCanceled and adding the corresponding StatusCanceled/EffectTriggerOnCanceled entry to cases, preserving coverage of all declared terminal outcomes.internal/daemon/loop_effect_relay_test.go-104-106 (1)
104-106: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAssert the error content of the simulated acknowledgement failure.
Line 104 checks only that the error is non-nil. The test passes if the drain fails for an unrelated reason, such as a listing failure. Assert that the error mentions the simulated acknowledgement failure and the delivery identifier.
As per path instructions: "Weak assertions like assert.Error(t, err) without message validation" are an anti-pattern to reject.
💚 Proposed assertion
- if _, err := relay.DrainPendingLoopEffects(t.Context(), looppkg.EffectDrainPage{}); err == nil { - t.Fatal("DrainPendingLoopEffects(first) error = nil, want simulated post-tool ack crash") - } + _, err := relay.DrainPendingLoopEffects(t.Context(), looppkg.EffectDrainPage{}) + if err == nil || !strings.Contains(err.Error(), "acknowledge loop effect") || + !strings.Contains(err.Error(), "simulated ack crash") { + t.Fatalf( + "DrainPendingLoopEffects(first) error = %v, want wrapped simulated post-tool ack crash", + err, + ) + }Add
"strings"to the imports.🤖 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 `@internal/daemon/loop_effect_relay_test.go` around lines 104 - 106, Strengthen the assertion in the DrainPendingLoopEffects test by validating that the returned error contains both the simulated post-tool acknowledgement failure text and the relevant delivery identifier, rather than checking only that it is non-nil. Add the strings import as needed and keep the existing failure expectation intact.Source: Path instructions
internal/daemon/loop_action_failure.go-52-65 (1)
52-65: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winKeep a non-empty
codeandrecoveryin the failure output.Two gaps exist in this segment:
- Line 58 assigns
codefromreasonErr.Codewithout an empty fallback. TheToolErrorbranch at Lines 67-70 does guard this case. IfreasonErr.Codeis empty or whitespace,NewActionFailurereceives an empty code, whilemarshalLoopActionFailureMetadatakeepsloopActionFailureCodebecause it checks for a non-empty value at Lines 33-36. The metadata code and the failure code then disagree.- Line 52 no longer requires
failure.Recovery, so Line 55 can overwrite the default recovery text with an empty string. Operators then get no recovery guidance.Apply the same fallback pattern used for
ToolError.🐛 Proposed fix for empty code and recovery
if provider, ok := errors.AsType[looppkg.SafeActionFailureProvider](cause); ok { failure := provider.SafeActionFailure() if strings.TrimSpace(failure.Code) != "" && strings.TrimSpace(failure.Cause) != "" { code = failure.Code message = failure.Cause - recovery = failure.Recovery + if strings.TrimSpace(failure.Recovery) != "" { + recovery = failure.Recovery + } } } else if reasonErr, ok := errors.AsType[*looppkg.ReasonError](cause); ok { - code = strings.TrimSpace(string(reasonErr.Code)) + if trimmed := strings.TrimSpace(string(reasonErr.Code)); trimmed != "" { + code = trimmed + } switch reasonErr.Code {🤖 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 `@internal/daemon/loop_action_failure.go` around lines 52 - 65, Update the failure handling around ReasonError and the preceding failure branch to preserve non-empty output: apply the same fallback used by the ToolError branch when trimmed reasonErr.Code is empty, and require a non-empty trimmed failure.Recovery before assigning it. Keep the existing default code and recovery values so NewActionFailure and marshalLoopActionFailureMetadata remain consistent.internal/daemon/loop_target_health_test.go-16-75 (1)
16-75: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winWrap the test body in a
t.Run("Should ...")subtest.The test body runs directly inside
TestBootDeadEntityRegistryShouldIsolateLoopTargetPolicywithout at.Run("Should ...")subtest. Other test files in this PR, such asloop_action_failure_test.go, use thet.Run("Should ...")pattern for each test case.🔧 Proposed fix
func TestBootDeadEntityRegistryShouldIsolateLoopTargetPolicy(t *testing.T) { t.Parallel() - - now := time.Date(2026, time.August, 2, 13, 0, 0, 0, time.UTC) - registry := &recordingRegistry{} - state := &bootState{ - ... - } - ... + + t.Run("Should isolate loop target policy from the shared breaker service", func(t *testing.T) { + t.Parallel() + now := time.Date(2026, time.August, 2, 13, 0, 0, 0, time.UTC) + registry := &recordingRegistry{} + state := &bootState{ + ... + } + ... + }) }As per path instructions, "MUST use t.Run("Should...") pattern for ALL test cases."
🤖 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 `@internal/daemon/loop_target_health_test.go` around lines 16 - 75, Wrap the existing body of TestBootDeadEntityRegistryShouldIsolateLoopTargetPolicy in a t.Run subtest named with the required “Should ...” pattern, preserving all setup, assertions, and test behavior inside the subtest.Source: Path instructions
internal/daemon/scheduler_loop_due_scan.go-41-64 (1)
41-64: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdvance the retry scan cursor only after a successful page.
EnqueueDueLoopRetryWakesPagereturns a zero-value cursor when its list query fails, butenqueueDueLoopRetryWakesstores that zero cursor before returning the error. That resets the due-retry scan to the start on the next backstop tick.🔧 Proposed fix
- _, next, err := dueWakes.EnqueueDueLoopRetryWakesPage( + _, next, err := dueWakes.EnqueueDueLoopRetryWakesPage( ctx, origin, now, s.loopRetryDueScan.cursor, defaultLoopRetryDueScanPageSize, ) - s.loopRetryDueScan.cursor = next - return err + if err != nil { + return err + } + s.loopRetryDueScan.cursor = next + return nil🤖 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 `@internal/daemon/scheduler_loop_due_scan.go` around lines 41 - 64, Update enqueueDueLoopRetryWakes so loopRetryDueScan.cursor is assigned the returned next cursor only when EnqueueDueLoopRetryWakesPage succeeds; preserve the existing cursor when the call returns an error, then return that error.internal/loop/action_test.go-517-554 (1)
517-554: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd a non-escalated failure to prove the filter.
The test name states that only escalated failures reach the repair prompt. The input contains one escalated failure only. If the disposition filter is removed, this test still passes. Add a second
ActionRepairFailurewith a non-escalated disposition, then assert that its cause does not appear in the prompt.💚 Proposed test extension
RepairFailures: []loop.ActionRepairFailure{{ NodeID: "primary", Disposition: loop.AttemptEscalated, Failure: loop.ClassifiedFailure{ Class: loop.FailurePayloadDeclared, Code: "payload_declared", Cause: "semantic escalation", Hint: "repair the payload", }, + }, { + NodeID: "secondary", + Failure: loop.ClassifiedFailure{ + Class: loop.FailureTransport, Code: "transport", + Cause: "unspecified disposition cause", Hint: "should be excluded", + }, }}, }) if err != nil { t.Fatalf("Execute() error = %v", err) } prompts := binder.promptMessages() + if len(prompts) == 1 && strings.Contains(prompts[0], "unspecified disposition cause") { + t.Fatalf("repair prompt = %q, want unspecified disposition excluded", prompts[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 `@internal/loop/action_test.go` around lines 517 - 554, Add a second ActionRepairFailure to the Execute input in the “Should pass only escalated classified failures to a repair generation” test, using a non-escalated disposition and a unique cause; extend the prompt assertions to verify that cause is absent while the existing escalated failure details remain present.internal/loop/coordinator_retry.go-53-65 (1)
53-65: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winEnforce retry deadlines when
FirstScheduledAtis nil.
planNodeRetryhas a configured deadline but skips the cutoff for any output withoutFirstScheduledAt, so scheduled retry backoff can resume after the node’s total deadline. Require a persisted first-scheduled timestamp for deadline-bearing cells, or useinput.nowas the fallback deadline origin in this retry path.🤖 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 `@internal/loop/coordinator_retry.go` around lines 53 - 65, Update planNodeRetry’s deadline enforcement so a configured policy.deadline is never skipped when input.output.FirstScheduledAt is nil: require and handle a persisted first-scheduled timestamp, or consistently use input.now as the fallback deadline origin, then apply the existing nextAttemptAt cutoff and FailureBudgetExhausted plan.internal/store/globaldb/global_db_loop_effect_outbox.go-186-233 (1)
186-233: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winHandle transient tool delivery failures without closing to
EffectFailed.
effectOutboxStateForOutcomemaps every non-OK result toEffectFailed, butloopEffectRelay.effectToolErrorcan produce tool-call errors such as context deadline exceeded. These rows keepstate = 'failed', so the dispatcher never reclaims them inDrainPendingLoopEffects; onlyattemptschanges. Either keep transient tool failures inpendingwith a retry cap or add a retryable failure state and a retry schedule.Also,
delivered_atis set for failed tool calls. Uselast_attempt_ator a separateclosed_atcolumn ifdelivered_atshould mark only successful effect delivery.🤖 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 `@internal/store/globaldb/global_db_loop_effect_outbox.go` around lines 186 - 233, Update effectOutboxStateForOutcome and the acknowledgement flow so transient tool-delivery errors such as context deadline exceeded remain retryable, using the existing pending/reclaim behavior or an equivalent retry state with a bounded retry policy. Ensure failed tool calls are not marked with delivered_at; reserve that timestamp for successful deliveries, or use the appropriate attempt/closure timestamp field instead.internal/task/manager_coordinator_run.go-94-96 (1)
94-96: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winSurface missing coordinator timer armer wiring.
coordinatorTimerArmeris left nil whennewLoopRetryTimerArmerreturns nil, andarmCoordinatorTimersstill returns nil for non-zeroplan.PostCommitTimers. Add a guard that returns an error or logs at warn level with the timer keys when post-commit timers are requested without an armer.🤖 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 `@internal/task/manager_coordinator_run.go` around lines 94 - 96, Update armCoordinatorTimers to detect when non-empty plan.PostCommitTimers or timers are requested while m.coordinatorTimerArmer is nil, and return an error or emit a warn-level log containing the timer keys instead of silently returning nil. Preserve the existing no-op behavior when no timers are requested.internal/loop/coordinator_test.go-510-512 (1)
510-512: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winTwo top-level tests miss the parallel default. Both tests run parallel subtests but do not call
t.Parallel()themselves, so the parent test blocks a test slot for the whole subtest group.
internal/loop/coordinator_test.go#L510-L512: addt.Parallel()as the first statement ofTestCoordinatorRunnerShouldKeepHealthyLaneRunningWhenTargetIsUnavailable.internal/loop/coordinator_test.go#L2426-L2428: addt.Parallel()as the first statement ofTestCoordinatorRunnerShouldIsolateRepeatedFailures.As per coding guidelines: "default
t.Parallel".🤖 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 `@internal/loop/coordinator_test.go` around lines 510 - 512, Add t.Parallel() as the first statement in both top-level tests: internal/loop/coordinator_test.go lines 510-512, TestCoordinatorRunnerShouldKeepHealthyLaneRunningWhenTargetIsUnavailable, and lines 2426-2428, TestCoordinatorRunnerShouldIsolateRepeatedFailures. No other test changes are needed.Source: Coding guidelines
internal/store/globaldb/schema/definitions/50_loops.sql-176-186 (1)
176-186: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winDefine and implement retention for
loop_admission_claims.
loop_admission_claimsis only present in the schema and integration test. The admission claim persists when itsloop_run_idorworkspace_idlater becomes invalid, and because the primary key includes no transient run id, the same(workspace_id, loop_name, source_key, event_key)can keep suppressing admissions indefinitely. Add an expiry/sweep path or FK constraints/cascades that match the intended lifecycle.🤖 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 `@internal/store/globaldb/schema/definitions/50_loops.sql` around lines 176 - 186, Define and implement lifecycle cleanup for loop_admission_claims so stale claims cannot suppress admissions indefinitely after their loop_run_id or workspace_id becomes invalid. Add the appropriate expiry/sweep mechanism or foreign-key constraints with cascading deletes, and wire it into the admission-claim lifecycle while preserving the existing primary-key behavior.internal/store/globaldb/global_db_loop_retry_due.go-274-287 (1)
274-287: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReport an absent current epoch instead of zero.
state.epochis asql.NullInt64filled by aLEFT JOIN. When the output row no longer exists,Validis false andstate.epoch.Int64is 0. The diagnostic payload then reportscurrent_epoch: 0, which reads as a real epoch. Epochs start at 1, pervalidateRetryDueCell. Emitnilwhen the value is absent so the diagnostic distinguishes "row gone" from "epoch advanced".🛡️ Proposed fix
+ var currentEpoch any + if state.epoch.Valid { + currentEpoch = state.epoch.Int64 + } return map[string]any{ loopRunEventPayloadKeyGeneration: cell.Generation, loopRunEventPayloadKeyNodeID: cell.NodeID, loopRunEventPayloadKeyItemIndex: cell.ItemIndex, watchEventsPayloadAttemptKey: cell.Attempt, loopRunEventPayloadKeyIssuedEpoch: cell.Epoch, - loopRunEventPayloadKeyCurrentEpoch: state.epoch.Int64, + loopRunEventPayloadKeyCurrentEpoch: currentEpoch, loopRunEventPayloadKeyScheduleKind: loopRetryScheduleKind, }🤖 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 `@internal/store/globaldb/global_db_loop_retry_due.go` around lines 274 - 287, Update staleRetrySchedulePayload to use state.epoch.Valid when populating loopRunEventPayloadKeyCurrentEpoch: emit the epoch value when valid, otherwise emit nil. Preserve the existing payload keys and ensure missing LEFT JOIN rows are distinguishable from a real epoch.internal/loop/node_requeue.go-55-67 (1)
55-67: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winNormalize the trimmed identity, or reject untrimmed values.
ValidateNodeRequeueMutationtrimsWorkspaceID,RunID, andNodeIDonly for the emptiness check. The mutation is a value, so the trimmed values are discarded. A caller that passes" looprun-1 "passes validation, andinternal/store/globaldb/global_db_loop_node_requeue.gothen binds the untrimmed value in itsloop_node_controlslookup. The request fails later withErrInvalidTransitionorErrRunNotFoundinstead of a clear validation error. Add a normalization step that the store applies before it validates.🛡️ Proposed fix
+// Normalize trims the requeue identity so store lookups bind canonical keys. +func (m NodeRequeueMutation) Normalize() NodeRequeueMutation { + m.WorkspaceID = WorkspaceID(strings.TrimSpace(string(m.WorkspaceID))) + m.RunID = RunID(strings.TrimSpace(string(m.RunID))) + m.NodeID = NodeID(strings.TrimSpace(string(m.NodeID))) + m.Reason = strings.TrimSpace(m.Reason) + return m +} + // ValidateNodeRequeueMutation rejects incomplete or stale node requeue requests. func ValidateNodeRequeueMutation(m NodeRequeueMutation) error {🤖 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 `@internal/loop/node_requeue.go` around lines 55 - 67, Update the node requeue store flow to trim and write back WorkspaceID, RunID, and NodeID on the NodeRequeueMutation before calling ValidateNodeRequeueMutation. Ensure validation and the subsequent loop_node_controls lookup use these normalized values, while preserving the existing validation for empty identities and other fields.internal/loop/node_attempt.go-84-90 (1)
84-90: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winReject a non-positive generation in the attempt ledger.
validateonly comparesa.Generationwith the caller-suppliedgeneration. If the caller passes0, the identity check passes and a row withgeneration = 0becomes persistable. Generation-scoped reads start at 1, so such a row is unreachable. Add an explicit lower bound.🛡️ Proposed fix
if a.LoopRunID != loopRunID || a.Generation != generation { return fmt.Errorf("%w: node attempt snapshot identity does not match generation", ErrValidation) } + if a.Generation < 1 { + return fmt.Errorf("%w: node attempt generation must be positive", ErrValidation) + } if a.NodeID == "" || a.ItemIndex < 0 || a.Attempt < 1 || a.StartedAt.IsZero() {🤖 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 `@internal/loop/node_attempt.go` around lines 84 - 90, Update NodeAttempt.validate to reject non-positive generation values before allowing persistence, including when the caller-supplied generation is 0. Preserve the existing identity comparison and validation errors, while ensuring valid generations remain 1 or greater.
🤖 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 `@internal/cli/config_test.go`:
- Around line 179-248: Update TestConfigSetShouldManageLoopLifecycleDefaults so
each entry in values runs inside its own t.Run using the required “Should...”
naming pattern, rather than sharing one subtest. Keep the existing config
set/get assertions and dependencies unchanged, while ensuring one failing path
does not prevent the remaining paths from executing.
In `@internal/config/tool_surface_loops.go`:
- Line 37: Update the configuration mappings for both
loops.defaults.delivery.predicates.cost_limit and
loops.defaults.watch.predicates.cost_limit to use unsigned-safe parsing with
explicit bounds validation, rejecting negative inputs and values above uint64’s
maximum so CostLimit == 0 remains reliable; adjust the relevant value-kind
implementation or add a dedicated unsigned value kind instead of ConfigValueInt.
In `@internal/daemon/loop_action_runtime.go`:
- Around line 196-203: In internal/daemon/loop_action_runtime.go lines 196-203,
update the timeout-reason selection after actionTimeoutSpecForRun so
actionTimeoutReasonForRun is called only when authoredTimeout is true; otherwise
use loopActionReasonNodeTimeout. In internal/daemon/loop_action_liveness.go
lines 279-296, document or enforce that actionTimeoutReasonForRun applies only
to authored timeouts, keeping its fallback consistent with the caller.
In `@internal/daemon/loop_effect_relay.go`:
- Around line 103-127: The drain loop around ListPendingLoopEffects and
AcknowledgeLoopEffect can reread unchanged pending entries indefinitely when no
acknowledgement succeeds. Add a termination condition, such as returning after a
full pass with no acknowledged entries or when len(entries) is less than limit,
while preserving report updates and existing error handling.
In `@internal/loop/coordinator_effects.go`:
- Around line 114-142: Move the len(node.OnQuarantine) == 0 early return in
attachQuarantineEffectIntents before scanning payload.Events, matching
attachRetryEffectIntents. Preserve the existing event matching, intent
rendering, and validation error behavior for nodes that declare quarantine
effects.
In `@internal/loop/coordinator_error_routes.go`:
- Around line 82-86: Update skipErrorRouteSuccessPath so ControlFanOut handling
skips or queues its fan-out body nodes before marking the associated collect
nodes skipped. Reuse the existing dependenciesSucceededForOutput and
collectDependenciesSucceeded conditions for the fan-out scope, preserving
collect execution until all body dependencies have succeeded.
In `@internal/loop/coordinator_lifecycle.go`:
- Around line 306-314: Update classifyGenerationOutputFailure so non-encoded
OutputRef values never become failure.Code: use the stable
FailurePayloadDeclared code and preserve the reference only in the failure
cause, truncating or otherwise bounding its length before storing it. Keep
encoded ActionFailure handling unchanged.
In `@internal/loop/coordinator_retry.go`:
- Around line 107-141: Use one authoritative retry attempt value for both
eligibility and backoff: update retryDelay and its callers to accept the output
attempt used by nodeFailureRetryEligible, and pass that value into
retrypkg.DelayInput instead of deriving it from len(attempts)+1. Keep attempts
only for previousRetryDelay.
In `@internal/loop/dsl/graph.go`:
- Around line 61-66: Update Node.Normalize to recursively normalize n.Body and
n.Contract in addition to initializing NodeLifecycleState. Ensure nested body
nodes and node-level contract extensions, including NodeDeadline,
ResultContract, OnError, TriggerEffects, and OnParentClose, are initialized
during normalization before promotion.
In `@internal/loop/effect_context.go`:
- Around line 115-126: Update effectRunLink to emit the registered loop-run
route prefix, including /api/workspaces/{workspace_id}/loop-runs/{run_id}, using
the available workspace identifier while preserving PathEscape and trimming of
runID. Keep effectDecisionLink’s query construction unchanged.
In `@internal/loop/generation_snapshot_intents.go`:
- Around line 241-250: Update the boundary-effect handling around the
Status.Terminal check in the snapshot validation flow so StatusNeedsApproval
effects are accepted, or remove the coordinator path that writes
approval-targeted effects to payload.BoundaryEffects. Preserve rejection of
other non-terminal statuses and ensure snapshots containing budgetStatus ==
StatusNeedsApproval no longer fail validation.
In `@internal/loop/node_requeue.go`:
- Around line 69-87: The pending requeue flow must preserve every node queued
for the same generation instead of returning only the first match. Update
pendingRequeueNode and its callers to collect and return all matching
control.NodeID values, then ensure the coordinator processes each pending node;
alternatively, make reserveNodeRequeue use a generation-only idempotency key so
only one node can reserve that wake.
In `@internal/loop/target_health.go`:
- Around line 77-105: Export actionTargetIdentity within the package as the
single source of rendered target identity. In internal/loop/target_health.go
lines 77-105, retain its namespace-aware derivation; in
internal/loop/coordinator_target_health.go lines 130-160, replace
staticTargetHealthKey’s raw-parameter logic with a call to actionTargetIdentity
using the attempt’s namespace, ensuring both paths construct the same
store.DeadEntityKey.
- Around line 46-53: Ensure target probe observations cannot leak from
admitActionTarget: avoid storing observations under an empty
strings.TrimSpace(taskRunID) key, remove the stored entry before returning
targetUnavailableError when decision.Allowed is false, and add cleanup in the
coordinator’s terminal task-run handling for observations that never reach
accountActionTarget/takeTargetProbe. Use the existing targetProbes and task-run
lifecycle symbols, preserving accounting for allowed, successfully dispatched
actions.
In `@internal/store/globaldb/global_db_loop_effect_outbox.go`:
- Around line 106-118: Move the static SQL used by ListPendingLoopEffects,
insertLoopEffectIntentsWithExecutor, and the acknowledge UPDATE into the
appropriate queries/loop_*.sql definitions, then run make codegen. Replace the
handwritten QueryContext/ExecContext calls with the generated package-private
sqlc methods, preserving existing parameters and repository result mappings;
leave only genuinely dynamic SQL handwritten.
In `@internal/store/globaldb/global_db_task_claim_complete.go`:
- Around line 161-169: Update the call to updateLoopNodeOutputStatusWithExecutor
in the coordinator-controlled completion path to retain its recorded result
alongside the error. Return the helper error first, then detect recorded ==
false and return an appropriate failure instead of reporting success; preserve
the existing successful return when the write was applied.
In `@internal/store/globaldb/global_db_task_claim_test.go`:
- Around line 7197-7204: In the GenerationSnapshotPayload test fixture, replace
the invalid NextAttemptAt initializer with a local variable holding
now.Add(time.Second), then assign its address via &nextAttemptAt while
preserving the existing timestamp value.
---
Minor comments:
In `@internal/daemon/loop_action_failure.go`:
- Around line 52-65: Update the failure handling around ReasonError and the
preceding failure branch to preserve non-empty output: apply the same fallback
used by the ToolError branch when trimmed reasonErr.Code is empty, and require a
non-empty trimmed failure.Recovery before assigning it. Keep the existing
default code and recovery values so NewActionFailure and
marshalLoopActionFailureMetadata remain consistent.
In `@internal/daemon/loop_effect_relay_test.go`:
- Around line 104-106: Strengthen the assertion in the DrainPendingLoopEffects
test by validating that the returned error contains both the simulated post-tool
acknowledgement failure text and the relevant delivery identifier, rather than
checking only that it is non-nil. Add the strings import as needed and keep the
existing failure expectation intact.
In `@internal/daemon/loop_target_health_test.go`:
- Around line 16-75: Wrap the existing body of
TestBootDeadEntityRegistryShouldIsolateLoopTargetPolicy in a t.Run subtest named
with the required “Should ...” pattern, preserving all setup, assertions, and
test behavior inside the subtest.
In `@internal/daemon/scheduler_loop_due_scan.go`:
- Around line 41-64: Update enqueueDueLoopRetryWakes so loopRetryDueScan.cursor
is assigned the returned next cursor only when EnqueueDueLoopRetryWakesPage
succeeds; preserve the existing cursor when the call returns an error, then
return that error.
In `@internal/loop/action_test.go`:
- Around line 517-554: Add a second ActionRepairFailure to the Execute input in
the “Should pass only escalated classified failures to a repair generation”
test, using a non-escalated disposition and a unique cause; extend the prompt
assertions to verify that cause is absent while the existing escalated failure
details remain present.
In `@internal/loop/coordinator_retry.go`:
- Around line 53-65: Update planNodeRetry’s deadline enforcement so a configured
policy.deadline is never skipped when input.output.FirstScheduledAt is nil:
require and handle a persisted first-scheduled timestamp, or consistently use
input.now as the fallback deadline origin, then apply the existing nextAttemptAt
cutoff and FailureBudgetExhausted plan.
In `@internal/loop/coordinator_test.go`:
- Around line 510-512: Add t.Parallel() as the first statement in both top-level
tests: internal/loop/coordinator_test.go lines 510-512,
TestCoordinatorRunnerShouldKeepHealthyLaneRunningWhenTargetIsUnavailable, and
lines 2426-2428, TestCoordinatorRunnerShouldIsolateRepeatedFailures. No other
test changes are needed.
In `@internal/loop/effects_dispatch_test.go`:
- Around line 119-239: Split
TestCoordinatorEffectsShouldAttachNodeAndTerminalTriggers into two t.Run
subtests using names beginning with “Should”: one for node lifecycle and
terminal trigger attachment, and one for quarantine context attachment. Keep
shared failureClass and nextAttemptAt declarations in the outer test, and move
each independent scenario’s setup, execution, and assertions into its
corresponding subtest.
- Around line 247-263: Add the canceled terminal outcome to
TestCoordinatorEffectsShouldAttachEveryDeclaredTerminalOutcome by configuring
dsl.TerminalEffects.OnCanceled and adding the corresponding
StatusCanceled/EffectTriggerOnCanceled entry to cases, preserving coverage of
all declared terminal outcomes.
In `@internal/loop/node_attempt.go`:
- Around line 84-90: Update NodeAttempt.validate to reject non-positive
generation values before allowing persistence, including when the
caller-supplied generation is 0. Preserve the existing identity comparison and
validation errors, while ensuring valid generations remain 1 or greater.
In `@internal/loop/node_requeue.go`:
- Around line 55-67: Update the node requeue store flow to trim and write back
WorkspaceID, RunID, and NodeID on the NodeRequeueMutation before calling
ValidateNodeRequeueMutation. Ensure validation and the subsequent
loop_node_controls lookup use these normalized values, while preserving the
existing validation for empty identities and other fields.
In `@internal/store/globaldb/global_db_loop_effect_outbox.go`:
- Around line 186-233: Update effectOutboxStateForOutcome and the
acknowledgement flow so transient tool-delivery errors such as context deadline
exceeded remain retryable, using the existing pending/reclaim behavior or an
equivalent retry state with a bounded retry policy. Ensure failed tool calls are
not marked with delivered_at; reserve that timestamp for successful deliveries,
or use the appropriate attempt/closure timestamp field instead.
In `@internal/store/globaldb/global_db_loop_retry_due.go`:
- Around line 274-287: Update staleRetrySchedulePayload to use state.epoch.Valid
when populating loopRunEventPayloadKeyCurrentEpoch: emit the epoch value when
valid, otherwise emit nil. Preserve the existing payload keys and ensure missing
LEFT JOIN rows are distinguishable from a real epoch.
In `@internal/store/globaldb/schema/definitions/50_loops.sql`:
- Around line 176-186: Define and implement lifecycle cleanup for
loop_admission_claims so stale claims cannot suppress admissions indefinitely
after their loop_run_id or workspace_id becomes invalid. Add the appropriate
expiry/sweep mechanism or foreign-key constraints with cascading deletes, and
wire it into the admission-claim lifecycle while preserving the existing
primary-key behavior.
In `@internal/task/manager_coordinator_run.go`:
- Around line 94-96: Update armCoordinatorTimers to detect when non-empty
plan.PostCommitTimers or timers are requested while m.coordinatorTimerArmer is
nil, and return an error or emit a warn-level log containing the timer keys
instead of silently returning nil. Preserve the existing no-op behavior when no
timers are requested.
---
Nitpick comments:
In `@internal/daemon/loop_action_liveness.go`:
- Around line 20-21: Rename loopActionReasonNodeDeadline to
loopActionReasonBudgetExhausted everywhere it is declared and referenced,
preserving its mapping to looppkg.FailureBudgetExhausted and the existing
emitted behavior.
In `@internal/daemon/loop_effect_relay_boot.go`:
- Around line 17-20: Update the registry type-check skip path in the boot relay
setup to emit a warning before returning nil when the registry does not
implement loopEffectStore. Reuse the existing coordinator or daemon logger and
clearly state that effect-outbox relay is disabled because the registry lacks
the required interface; leave the successful setup and hard tool-caller error
paths unchanged.
In `@internal/daemon/loop_effect_relay_test.go`:
- Around line 20-21: Wrap each of the five top-level test bodies in
internal/daemon/loop_effect_relay_test.go with a t.Run subtest named “Should …”,
preserving the existing test logic inside the corresponding subtest and ensuring
every test case follows the required t.Run pattern.
In `@internal/daemon/loop_effect_relay.go`:
- Around line 65-87: Update the effect-delivery path used by loopEffectRelay.Run
and DrainPendingLoopEffects to apply a per-entry timeout around each target
Call, using context.WithTimeout and cancelling it after the call completes.
Preserve the daemon context for cancellation while ensuring a slow individual
tool call cannot hold drainMu and block delivery of subsequent pending effects.
In `@internal/daemon/task_runtime_test.go`:
- Around line 154-175: Add coverage for ActionRunTimeoutReason in the liveness
tests by introducing a runner stub that implements the interface and returns a
non-empty timeout reason, then use it in a timeout scenario and assert
executeQueuedRun reports that reason instead of loopActionReasonNodeTimeout. Add
a second case where the interface returns an empty reason and assert
actionTimeoutReasonForRun falls back to loopActionReasonNodeTimeout, ensuring
both branches fail if the business logic changes.
In `@internal/loop/action_failure.go`:
- Around line 80-119: Extract the duplicated trimming and Kind/Code/Cause
validation from ActionFailureFromMetadata and ActionFailureOutputRef into one
shared helper, then call that helper from both functions. Preserve each
function’s existing success and failure return behavior, including Recovery
trimming and output encoding.
In `@internal/loop/config.go`:
- Around line 247-262: Update mergeLifecycleLayer so a non-empty layer.Autopause
replaces target.Autopause rather than appending to it, preserving the
later-layer override semantics and ordered first-match-wins behavior of
LoopAutopauseRule.
In `@internal/loop/control_plan_finish.go`:
- Around line 77-83: Update generationOutputsExpectCurrentEpoch to avoid
mutating the caller-provided outputs slice or its GenerationOutput elements:
create and return a cloned slice with each cloned output’s ExpectedEpoch set
from its Epoch, preserving the existing values and length.
In `@internal/loop/coordinator_action.go`:
- Around line 187-227: The liveness path currently recomputes the same
action-run limit through both ActionRunTimeout and ActionRunTimeoutReason.
Expose a single coordinator method returning duration, enabled, and reasonCode
from one actionRunLimit evaluation, then update callers to reuse that combined
result and avoid duplicate resolveActionRun and ListGenerationOutputs reads.
In `@internal/loop/coordinator_lifecycle_test.go`:
- Around line 360-473: Split
TestCoordinatorQuarantineShouldPreserveDiagnosableSanitizedHistory into two
t.Run subtests named with the required “Should…” pattern: one containing the
sanitized appended-history and mutation/event assertions, and another containing
the bounded encoding and truncation-marker assertions. Keep the shared setup
scoped appropriately so each subtest independently identifies its failing
behavior.
In `@internal/loop/coordinator_lifecycle.go`:
- Around line 45-63: Batch or cache task-run lookups during the lifecycle pass
around applyTerminalNodeLifecycle so each TaskRunID is read at most once per
coordinator pass, rather than once for every terminal output. Reuse the fetched
task-run data when processing advanced entries and preserve the existing
lifecycle results and error propagation.
In `@internal/loop/coordinator_options.go`:
- Around line 54-66: Update WithCoordinatorNodeAttemptReader and
WithCoordinatorNodeControlReader to assign their readers only when the supplied
interface is non-nil, matching WithCoordinatorRetryRand. Preserve the upgraded
readers initialized by NewCoordinatorRunner when callers pass nil.
In `@internal/loop/coordinator_quarantine.go`:
- Around line 79-87: Update applyQuarantineToPlan to clone the outputs before
applying quarantine mutations, then modify and return that cloned slice rather
than the caller-owned backing array. Reuse cloneGenerationOutputs for the clone
and ensure payload.Outputs and the function result both reference the updated
clone.
In `@internal/loop/coordinator_requeue.go`:
- Around line 18-24: Update the control lookup in the requeue flow to call the
existing r.listNodeControls helper instead of checking r.controls and invoking
ListNodeControls directly. Preserve the helper’s returned controls and error
handling so the shared nil-check behavior and “loop: list node controls: %w”
error format are used.
In `@internal/loop/coordinator_test.go`:
- Around line 2595-2704: The test currently combines two behaviors without
subtests. Split TestCoordinatorRunnerShouldPlanRequeueThroughSuccession into
t.Run subtests using names beginning with “Should”: one covering the
generation-3 requeue succession plan and provenance assertions, and another
covering the post-requeue target-health probe and safe target_unavailable
failure during ExecuteActionRun; preserve the existing setup and assertions
within the appropriate phases.
In `@internal/loop/dsl/refs/condition.go`:
- Around line 44-46: Update the boolean validation in the condition evaluation
flow around c.Raw to use a type-based check for the CEL types.Bool value instead
of comparing against types.True and types.False singletons, while preserving the
existing error and successful evaluation behavior.
In `@internal/loop/dsl/refs/diagnostics.go`:
- Around line 33-39: Extract a small generic key-collection helper in the
diagnostics code that accepts a map with any value type, collects its keys,
sorts them, and returns the sorted slice. Update availableSchemaFields,
availablePropertyKeys, and sortedMapKeys to delegate to this helper while
preserving their existing outputs and behavior.
In `@internal/loop/generation_snapshot_attempts.go`:
- Around line 22-44: Move the loop_node_attempts INSERT from the handwritten
transaction code into internal/store/globaldb/queries as an idempotent generated
query matching the existing LoopNodeAttempt model and ListLoopNodeAttempts seam.
Regenerate via make codegen, update the internal/loop flow to invoke the
generated globaldb store method, and remove the direct tx.ExecContext INSERT
while preserving all inserted fields and conflict behavior.
In `@internal/loop/generation_snapshot_controls.go`:
- Around line 35-37: Update the affected-row handling around the insert mutation
so affected == 0 reports that a node control row already exists, while
preserving the existing revision-changed error for other unexpected affected
counts. Use the visible mutation and ErrTransitionConflict symbols to
distinguish the insert conflict from genuine revision failures.
In `@internal/loop/generation_snapshot_intents.go`:
- Around line 76-82: Update the snapshot cloning logic around the Failure and
Effects handling to deep-clone nested payloads rather than only copying structs
or slice headers. Clone each RenderedEffectIntent.Entry json.RawMessage backing
array, and clone all slice/map fields nested in ClassifiedFailure, matching the
explicit Entry cloning used by the boundary-effect path.
In `@internal/loop/generation_snapshot.go`:
- Around line 297-313: Update validate to compute one trimmed status value
before the retry-specific check, then reuse that value for both the
generationOutputRetrying comparison and the subsequent status switch. Ensure
direct validation of whitespace-padded retrying statuses still requires
NextAttemptAt.
In `@internal/loop/linter_effects.go`:
- Around line 17-24: Update lintEffect to validate effect shape with separate,
clearly ordered checks: report a distinct diagnostic when both tool and emit are
set, when neither is set, when emit.Kind is blank, and when an emit includes
With entries. Replace the combined boolean and generic message with
rule-specific diagnostics while preserving the existing early-return behavior
for invalid effects.
In `@internal/loop/linter_test.go`:
- Around line 1653-1656: Update the test fixture setup around invalidValue so it
constructs a fresh definition rather than assigning invalidValue := valid;
ensure the graph and its nodes use independent storage before setting
OnParentClose to "detach", while preserving the existing valid fixture
unchanged.
In `@internal/loop/target_health_test.go`:
- Around line 17-19: Wrap the body of
TestTargetHealthShouldFailFastThroughTheNormalFailureChain in a t.Run subtest
named with the required “Should …” pattern, moving all existing setup and
assertions into that callback while preserving the test’s behavior.
- Around line 56-59: Update the terminal assertion in the target-unavailable
test to remove the unreachable terminal.ReasonCode == circuitBreakerReasonCode
clause, or replace it with a separate assertion that directly verifies
circuit-breaker precedence using an appropriate breaker-open fixture. Keep the
existing failed-status and target-unavailable reason checks intact.
In `@internal/loop/types.go`:
- Around line 170-174: Connect the production tool-schema source used by the
linter’s tools field to WatchIdentitySource by implementing
SupportsStableWatchIdentity(kind string) bool. Return the correct stability
result for each supported tool kind so linter_lifecycle.go can emit
CodeWatchIdentityRequired for that source.
In `@internal/store/globaldb/global_db_loop_node_requeue.go`:
- Around line 193-200: Update the node requeue run-event payload in the
surrounding requeue handler to stop embedding prepared.entry; emit only a
compact reference or digest that identifies the quarantine control row, while
preserving the existing generation, node, actor, and bounded reason fields so
readers can fetch the full entry separately.
- Around line 162-177: Update the dependency-attention cleanup near the requeue
handling to persist the parked producer node ID in a structured field and match
that field when clearing dependency_quarantined attention, instead of using the
attention_reason suffix and attentionSuffix wording. Update the related
write/read paths and add coverage proving cleanup still works when the
human-readable attention wording changes.
In `@internal/store/globaldb/global_db_loop_node_terminal.go`:
- Around line 41-55: Update recordLoopNodeTerminalWithExecutor to return early
for non-worker runs before trimming run.LoopRunID, then decode and validate
run.Metadata once into loopNodeRunMetadata. Pass that metadata to both
updateLoopNodeOutputStatusWithExecutor and
appendLoopNodeLateArrivalWithExecutor, and adjust those helpers to reuse it
instead of decoding and validating independently.
In `@internal/store/globaldb/global_db_loop_retry_due.go`:
- Around line 71-85: Replace the hardcoded excluded run-status list in the retry
query around the global-db retry selection with values derived from the
looppkg.Status liveness rule used near line 259, binding or constructing the SQL
parameters so Go constants remain authoritative. Also update
internal/loop/generation_snapshot_controls.go lines 73-88 to bind
NodeControlMutationQuarantine and the attention mutation kind as parameters in
every corresponding CASE branch, rather than embedding the string literals; both
sites require direct changes.
In `@internal/store/globaldb/global_db_loop_schema_integration_test.go`:
- Around line 408-413: Improve the failure message in the migration assertion by
including all scanned values checked by the condition—status, outputRef,
taskRunID, resolvedRuntime, attempt, nextAttemptAt, firstScheduledAt, epoch,
goalStatus, goalTurnsUsed, and goalTurnLimit—so the failing column can be
identified directly.
In `@internal/store/globaldb/global_db_loop_test.go`:
- Around line 324-424: Split
TestGlobalDBLoopEffectAcknowledgementShouldBeAtomicAndIdempotent into t.Run
subtests named with the “Should …” pattern, covering durable seeding,
first-winner acknowledgement, replay-loser idempotency, delivered outbox state,
and exactly-once delivery keys. Keep the existing setup and assertions intact
while grouping each invariant so failures identify the affected phase.
- Around line 127-130: Update the control assertions in the test cases around
the existing checks, including the analogous checks near lines 257, 283, and
316, to locate each control by its NodeID rather than slice position. Build or
use a NodeID-to-control lookup for the results from
ListNodeControls/ListLoopNodeControls, then assert the expected count and
properties for finish and other_consumer independently of returned ordering.
In `@internal/store/globaldb/global_db_task_coordinator_effects.go`:
- Around line 18-26: Define dedicated loop event payload key constants alongside
the existing loopRunEventPayloadKey symbols for next_attempt_at, failure_class,
disposition, entry, family, and target, then replace the corresponding inline
literals in the payload construction sites. Add a breaker-specific state key
constant and update the breaker-state handling near network wake processing to
use it instead of networkWakeEventStateKey, keeping the vocabularies separate.
In `@internal/store/globaldb/schema/definitions/50_loops.sql`:
- Around line 521-538: The new partial indexes
idx_loop_node_controls_quarantined, idx_loop_node_controls_attention, and
idx_loop_effect_outbox_pending need lookup and ordering columns in their indexed
keys so scans can filter by loop_run_id and order by created_at without table
lookups. Add a new append-only migration under the migrations history that drops
and recreates these indexes with the columns required by their query patterns,
preserving each partial predicate; do not edit 00043_schema.sql or
00044_schema.sql.
In `@internal/task/coordinator_timer.go`:
- Around line 40-46: Update CoordinatorTimerSpec.Validate to validate each timer
identity field separately and include the corresponding field name in the
ErrValidation message for the first invalid value, rather than using one
combined condition. Preserve the existing validation rules and path context for
LoopRunID, NodeID, Generation, ItemIndex, Attempt, IssuedEpoch, FireAt, and
IdempotencyKey.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 70956d3f-ed44-407f-9f06-d016841a2cdf
⛔ Files ignored due to path filters (21)
docs/qa/bugs/BUG-20260802-loop-lifecycle-config-unsupported.mdis excluded by!**/*.mddocs/qa/charters/CH-loop-lifecycle-config-cli.mdis excluded by!**/*.mddocs/qa/charters/CH-loop-quarantine-repair.mdis excluded by!**/*.mddocs/qa/journeys/J-bound-runaway-work.mdis excluded by!**/*.mddocs/qa/journeys/J-recover-loop-node-failure.mdis excluded by!**/*.mddocs/qa/journeys/J-tune-loop-lifecycle-defaults.mdis excluded by!**/*.mddocs/qa/reports/2026-08-02-loop-lifecycle-config.mdis excluded by!**/*.mddocs/qa/scenarios/LP-error-route-fallback.mdis excluded by!**/*.mddocs/qa/scenarios/LP-loop-lifecycle-config-cli.mdis excluded by!**/*.mddocs/qa/scenarios/LP-on-error-notification-with-context.mdis excluded by!**/*.mddocs/qa/scenarios/LP-quarantine-diagnose-requeue.mdis excluded by!**/*.mddocs/qa/scenarios/LP-sick-target-degrades-one-lane.mdis excluded by!**/*.mddocs/qa/scenarios/LP-terminal-outcome-notification.mdis excluded by!**/*.mddocs/qa/scenarios/LP-transient-blip-heals.mdis excluded by!**/*.mddocs/qa/scenarios/LP-unannotated-escalation.mdis excluded by!**/*.mddocs/qa/scenarios/TA-loop-failure-breaker.mdis excluded by!**/*.mdinternal/store/globaldb/schema/migrations/atlas.sumis excluded by!**/*.sum,!**/*.suminternal/testutil/acpmock/testdata/loop_node_lifecycle_fixture.jsonis excluded by!**/*.jsonopenapi/compozy.jsonis excluded by!**/*.jsonweb/src/generated/compozy-openapi.d.tsis excluded by!**/generated/**,!**/generated/**,!**/*.d.tsweb/src/generated/loop-enums.tsis excluded by!**/generated/**,!**/generated/**
📒 Files selected for processing (181)
internal/api/contract/loop_enums.gointernal/api/core/loops_test.gointernal/cli/config_path_classification.gointernal/cli/config_test.gointernal/config/config.gointernal/config/loop_autopause.gointernal/config/loop_lifecycle.gointernal/config/loops.gointernal/config/loops_test.gointernal/config/merge_loops.gointernal/config/tool_surface_loops.gointernal/config/tool_surface_test.gointernal/daemon/boot.gointernal/daemon/boot_components.gointernal/daemon/boot_dead_entity.gointernal/daemon/boot_publish.gointernal/daemon/daemon.gointernal/daemon/daemon_shutdown_runtime.gointernal/daemon/daemon_shutdown_targets.gointernal/daemon/loop_action_failure.gointernal/daemon/loop_action_failure_test.gointernal/daemon/loop_action_liveness.gointernal/daemon/loop_action_runtime.gointernal/daemon/loop_defaults.gointernal/daemon/loop_defaults_test.gointernal/daemon/loop_effect_relay.gointernal/daemon/loop_effect_relay_boot.gointernal/daemon/loop_effect_relay_test.gointernal/daemon/loop_goal_command_definition.gointernal/daemon/loop_goal_executor_test.gointernal/daemon/loop_goal_managed_runtime_integration_test.gointernal/daemon/loop_node_lifecycle_e2e_integration_test.gointernal/daemon/loop_target_health.gointernal/daemon/loop_target_health_test.gointernal/daemon/scheduler_loop_coordinator.gointernal/daemon/scheduler_loop_due_scan.gointernal/daemon/scheduler_loop_retry_timer.gointernal/daemon/scheduler_runtime.gointernal/daemon/scheduler_runtime_test.gointernal/daemon/scheduler_task_source_types.gointernal/daemon/settings_runtime_applier.gointernal/daemon/task_runtime_boot.gointernal/daemon/task_runtime_boot_roles.gointernal/daemon/task_runtime_test.gointernal/deadentity/service_test.gointernal/loop/action_failure.gointernal/loop/action_repair_feedback.gointernal/loop/action_retry_feedback.gointernal/loop/action_runagent.gointernal/loop/action_test.gointernal/loop/action_types.gointernal/loop/admission_claim.gointernal/loop/api_types.gointernal/loop/cancellation.gointernal/loop/config.gointernal/loop/constants.gointernal/loop/control_namespace.gointernal/loop/control_namespace_history.gointernal/loop/control_namespace_history_test.gointernal/loop/control_namespace_test.gointernal/loop/control_plan.gointernal/loop/control_plan_finish.gointernal/loop/control_readiness.gointernal/loop/control_tasks.gointernal/loop/coordinator.gointernal/loop/coordinator_action.gointernal/loop/coordinator_effects.gointernal/loop/coordinator_error_routes.gointernal/loop/coordinator_generation.gointernal/loop/coordinator_generation_idle.gointernal/loop/coordinator_generation_reattempt.gointernal/loop/coordinator_generation_succession.gointernal/loop/coordinator_goal_control_test.gointernal/loop/coordinator_history.gointernal/loop/coordinator_lifecycle.gointernal/loop/coordinator_lifecycle_test.gointernal/loop/coordinator_metadata.gointernal/loop/coordinator_options.gointernal/loop/coordinator_outputs.gointernal/loop/coordinator_quarantine.gointernal/loop/coordinator_requeue.gointernal/loop/coordinator_retry.gointernal/loop/coordinator_retry_test.gointernal/loop/coordinator_snapshot_test.gointernal/loop/coordinator_succession.gointernal/loop/coordinator_target_health.gointernal/loop/coordinator_terminal_helpers.gointernal/loop/coordinator_test.gointernal/loop/dsl/codec_test.gointernal/loop/dsl/contract.gointernal/loop/dsl/effects.gointernal/loop/dsl/graph.gointernal/loop/dsl/lifecycle.gointernal/loop/dsl/node_params.gointernal/loop/dsl/refs/condition.gointernal/loop/dsl/refs/diagnostics.gointernal/loop/dsl/refs/namespace.gointernal/loop/dsl/refs/refs_test.gointernal/loop/dsl/types_nodes.gointernal/loop/dsl/wait.gointernal/loop/effect_context.gointernal/loop/effect_outbox.gointernal/loop/effects_dispatch.gointernal/loop/effects_dispatch_test.gointernal/loop/errors.gointernal/loop/executed_definition_snapshot.gointernal/loop/failure_class.gointernal/loop/failure_classify.gointernal/loop/failure_classify_test.gointernal/loop/failure_repair.gointernal/loop/failure_repair_test.gointernal/loop/failure_result_contract.gointernal/loop/failure_result_contract_test.gointernal/loop/generation_intent.gointernal/loop/generation_snapshot.gointernal/loop/generation_snapshot_attempts.gointernal/loop/generation_snapshot_controls.gointernal/loop/generation_snapshot_intents.gointernal/loop/generation_snapshot_test.gointernal/loop/lifecycle_config.gointernal/loop/linter_contract.gointernal/loop/linter_effects.gointernal/loop/linter_lifecycle.gointernal/loop/linter_test.gointernal/loop/linter_wait.gointernal/loop/node_attempt.gointernal/loop/node_control.gointernal/loop/node_control_mutation.gointernal/loop/node_requeue.gointernal/loop/node_wait.gointernal/loop/predicate_policy.gointernal/loop/predicate_policy_test.gointernal/loop/quarantine.gointernal/loop/retry_due.gointernal/loop/service_test.gointernal/loop/service_types.gointernal/loop/target_health.gointernal/loop/target_health_test.gointernal/loop/types.gointernal/loop/watch_events_registry_phase_a.gointernal/store/globaldb/global_db_goal_reentry_test.gointernal/store/globaldb/global_db_goal_turn_runtime_integration_test.gointernal/store/globaldb/global_db_loop.gointernal/store/globaldb/global_db_loop_admission_claims.gointernal/store/globaldb/global_db_loop_effect_outbox.gointernal/store/globaldb/global_db_loop_events.gointernal/store/globaldb/global_db_loop_node_attempts.gointernal/store/globaldb/global_db_loop_node_controls.gointernal/store/globaldb/global_db_loop_node_requeue.gointernal/store/globaldb/global_db_loop_node_terminal.gointernal/store/globaldb/global_db_loop_node_waits.gointernal/store/globaldb/global_db_loop_retry_due.gointernal/store/globaldb/global_db_loop_scan.gointernal/store/globaldb/global_db_loop_schema_integration_test.gointernal/store/globaldb/global_db_loop_test.gointernal/store/globaldb/global_db_task_claim_complete.gointernal/store/globaldb/global_db_task_claim_test.gointernal/store/globaldb/global_db_task_coordinator_boundaries.gointernal/store/globaldb/global_db_task_coordinator_effects.gointernal/store/globaldb/global_db_task_coordinator_intents.gointernal/store/globaldb/global_db_task_coordinator_runtime.gointernal/store/globaldb/loop_sqlc_mapping.gointernal/store/globaldb/queries/loop_core.sqlinternal/store/globaldb/queries/loop_lifecycle.sqlinternal/store/globaldb/queries/loop_runtime.sqlinternal/store/globaldb/queries/task_claim.sqlinternal/store/globaldb/schema/definitions/41_reliability.sqlinternal/store/globaldb/schema/definitions/50_loops.sqlinternal/store/globaldb/schema/migrations/00043_schema.sqlinternal/store/globaldb/schema/migrations/00044_schema.sqlinternal/store/globaldb/sqlcgen/loop_core.sql.gointernal/store/globaldb/sqlcgen/loop_lifecycle.sql.gointernal/store/globaldb/sqlcgen/loop_runtime.sql.gointernal/store/globaldb/sqlcgen/models.gointernal/store/globaldb/sqlcgen/task_claim.sql.gointernal/store/types_dead_entity.gointernal/task/coordinator.gointernal/task/coordinator_timer.gointernal/task/manager.gointernal/task/manager_constructor.gointernal/task/manager_coordinator_run.go
💤 Files with no reviewable changes (2)
- internal/store/globaldb/queries/task_claim.sql
- internal/store/globaldb/sqlcgen/task_claim.sql.go
|
CodeRabbit round 1 was remediated in e41e4c1.
Focused verification: 5,023 tests passed across 11 affected packages; codegen-check and diff-check passed. The consolidated full gate remains scheduled for the final stack layer. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
internal/loop/coordinator_lifecycle_test.go (1)
272-276: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSet
nowon the hand-built runner.This runner leaves
nownil. The test passes only becauseGetTaskRunfails beforeapplyTerminalNodeLifecyclecallsr.now(). If the call order changes, the test panics instead of reporting a clear failure.♻️ Proposed change
runner := &CoordinatorRunner{ taskRuns: &coordinatorRunnerTaskRunReader{runs: map[string]task.Run{}}, targetHealth: newRecordingTargetHealth(), } + runner.now = func() time.Time { return firstScheduledAt }🤖 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 `@internal/loop/coordinator_lifecycle_test.go` around lines 272 - 276, Initialize the now field on the hand-built CoordinatorRunner in this test with the appropriate clock function or test time provider, alongside taskRuns and targetHealth, so applyTerminalNodeLifecycle can safely call r.now() even if GetTaskRun succeeds or call order changes.
🤖 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 `@internal/config/tool_surface_test.go`:
- Around line 688-691: Strengthen the invalid uint64 cases in the test around
NormalizeToolConfigValue by asserting the specific expected validation error for
each input, using ErrorContains or ErrorAs rather than only checking err != nil.
Split the cases as needed so "-1", the overflowing integer string, and the
imprecise float each verify their appropriate error.
In `@internal/loop/coordinator_lifecycle.go`:
- Around line 44-50: Update applyTerminalNodeLifecycle so readGenerationHistory
runs whenever generation history is needed, regardless of whether r.targetHealth
is nil; retain the r.targetHealth == nil guard only around target-accounting
operations, ensuring runtimeNamespaceWithHistory receives the
previous-generation records for generation > 1.
In `@internal/loop/coordinator_test.go`:
- Around line 2711-2714: Add t.Parallel() at the start of the top-level
TestCoordinatorRunnerShouldPlanEveryPendingRequeueInOneGeneration function,
before its nested t.Run, matching the neighboring top-level tests.
In `@internal/loop/target_health_test.go`:
- Around line 179-180: Add t.Parallel() at the beginning of
TestTargetHealthShouldReuseRenderedIdentityWithoutProbeMemory, while preserving
the existing parallel child test and isolated-state behavior.
---
Nitpick comments:
In `@internal/loop/coordinator_lifecycle_test.go`:
- Around line 272-276: Initialize the now field on the hand-built
CoordinatorRunner in this test with the appropriate clock function or test time
provider, alongside taskRuns and targetHealth, so applyTerminalNodeLifecycle can
safely call r.now() even if GetTaskRun succeeds or call order changes.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e10e9a72-3efb-4adc-aebe-4868eafc0467
⛔ Files ignored due to path filters (2)
.codex/scripts/coderabbit-review-graphql.shis excluded by!.codex/**internal/store/globaldb/schema/migrations/atlas.sumis excluded by!**/*.sum,!**/*.sum
📒 Files selected for processing (58)
internal/cli/config.gointernal/cli/config_path_classification.gointernal/cli/config_roles.gointernal/cli/config_test.gointernal/cli/config_value_parse.gointernal/config/tool_surface.gointernal/config/tool_surface_coercion.gointernal/config/tool_surface_loops.gointernal/config/tool_surface_test.gointernal/config/tool_surface_types.gointernal/daemon/loop_action_failure.gointernal/daemon/loop_action_failure_test.gointernal/daemon/loop_action_runtime.gointernal/daemon/loop_effect_relay.gointernal/daemon/loop_effect_relay_test.gointernal/daemon/loop_target_health_test.gointernal/daemon/scheduler_loop_due_scan.gointernal/daemon/scheduler_runtime_test.gointernal/daemon/task_runtime_test.gointernal/loop/action_repair_feedback.gointernal/loop/action_test.gointernal/loop/admission_claim.gointernal/loop/coordinator_effects.gointernal/loop/coordinator_error_routes.gointernal/loop/coordinator_lifecycle.gointernal/loop/coordinator_lifecycle_test.gointernal/loop/coordinator_outputs.gointernal/loop/coordinator_requeue.gointernal/loop/coordinator_retry.gointernal/loop/coordinator_retry_test.gointernal/loop/coordinator_target_health.gointernal/loop/coordinator_test.gointernal/loop/dsl/graph.gointernal/loop/dsl/types_test.gointernal/loop/effect_context.gointernal/loop/effects_dispatch_test.gointernal/loop/generation_snapshot_attempts.gointernal/loop/generation_snapshot_intents.gointernal/loop/generation_snapshot_test.gointernal/loop/node_attempt.gointernal/loop/node_requeue.gointernal/loop/target_health.gointernal/loop/target_health_test.gointernal/store/globaldb/global_db_loop_admission_claims.gointernal/store/globaldb/global_db_loop_effect_outbox.gointernal/store/globaldb/global_db_loop_node_requeue.gointernal/store/globaldb/global_db_loop_retry_due.gointernal/store/globaldb/global_db_loop_schema_integration_test.gointernal/store/globaldb/global_db_loop_test.gointernal/store/globaldb/global_db_task_claim_complete.gointernal/store/globaldb/global_db_task_claim_test.gointernal/store/globaldb/queries/loop_lifecycle.sqlinternal/store/globaldb/schema/definitions/50_loops.sqlinternal/store/globaldb/schema/migrations/00045_schema.sqlinternal/store/globaldb/sqlcgen/loop_lifecycle.sql.gointernal/store/globaldb/sqlcgen/models.gointernal/task/manager_coordinator_run.gointernal/task/manager_test.go
🚧 Files skipped from review as they are similar to previous changes (33)
- internal/loop/action_test.go
- internal/config/tool_surface_loops.go
- internal/loop/admission_claim.go
- internal/daemon/loop_action_runtime.go
- internal/daemon/scheduler_loop_due_scan.go
- internal/loop/coordinator_target_health.go
- internal/loop/effect_context.go
- internal/loop/action_repair_feedback.go
- internal/loop/generation_snapshot_attempts.go
- internal/cli/config_path_classification.go
- internal/store/globaldb/global_db_task_claim_complete.go
- internal/loop/node_requeue.go
- internal/loop/coordinator_effects.go
- internal/store/globaldb/global_db_loop_node_requeue.go
- internal/loop/coordinator_requeue.go
- internal/loop/effects_dispatch_test.go
- internal/task/manager_coordinator_run.go
- internal/loop/node_attempt.go
- internal/loop/coordinator_retry_test.go
- internal/loop/coordinator_outputs.go
- internal/loop/coordinator_error_routes.go
- internal/loop/dsl/graph.go
- internal/daemon/loop_target_health_test.go
- internal/daemon/loop_action_failure.go
- internal/loop/coordinator_retry.go
- internal/loop/target_health.go
- internal/daemon/loop_effect_relay.go
- internal/store/globaldb/global_db_loop_retry_due.go
- internal/store/globaldb/global_db_loop_test.go
- internal/store/globaldb/schema/definitions/50_loops.sql
- internal/daemon/scheduler_runtime_test.go
- internal/store/globaldb/global_db_loop_effect_outbox.go
- internal/store/globaldb/sqlcgen/models.go
Stack context
This is 1/3 of the Loop node lifecycle stack and replaces the oversized review surface from #294.
mainReview and merge from the bottom upward. GitHub records this chain as native stack #303.
Summary
This layer establishes the durable runtime foundation for the complete Loop node lifecycle contract. It teaches the daemon how to classify failures, apply one deterministic handling precedence, persist retry/effect/health state, isolate side effects through an outbox, and quarantine unhealthy targets without collapsing the whole Loop run.
The later PRs expose these primitives through agent-manageable surfaces and the Web UI. This PR intentionally stops at the runtime boundary so reviewers can evaluate persistence and coordinator semantics independently.
What changes
Failure classification and authoring contract
compozy.loop/v1grammar with result contracts, retry/backoff policy, time limits, error routes, effects, waits, and node-level lifecycle policy.Deterministic failure precedence
Durable effects
emitand daemon-tool effects through an isolated relay with stable delivery keys.Target health and quarantine
Storage and migrations
mainhistory; no existing migration bytes or Atlas history are rewritten.Runtime safety invariants
Verification
main.make gate-fullis intentionally deferred until after CodeRabbit remediation, per the delivery decision for this review-first stack. No final green-gate claim is made by this PR description.Compozy Impact Audit
Reviewer guide
Suggested order: schema/migrations → failure classifier → coordinator precedence → effect transaction/relay → target health/quarantine. The most important review questions are transaction boundaries, replay identity, epoch fencing, and whether every partial failure leaves a truthful durable state.
Stack created with the official GitHub Stacks CLI.
Summary by CodeRabbit
New Features
Bug Fixes