Skip to content

feat: add Loop lifecycle runtime foundations (stack 1/3) - #300

Merged
pedronauck merged 6 commits into
mainfrom
stack/loop-lifecycle-v2-01-runtime
Aug 3, 2026
Merged

pedronauck merged 6 commits into
mainfrom
stack/loop-lifecycle-v2-01-runtime

Conversation

@pedronauck

@pedronauck pedronauck commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Stack context

This is 1/3 of the Loop node lifecycle stack and replaces the oversized review surface from #294.

Layer Pull request Base CodeRabbit-visible files
1. Runtime foundations #300 (this PR) main 184
2. Controls and public surfaces #301 #300 253
3. Web, docs, and QA #302 #301 227

Review 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

  • Adds a closed failure vocabulary with evidence, remediation hints, payload-declared failure detection, and a fixed classification order.
  • Extends the compozy.loop/v1 grammar with result contracts, retry/backoff policy, time limits, error routes, effects, waits, and node-level lifecycle policy.
  • Keeps authoring diagnostics distinct from runtime failure classification.
  • Adds config defaults and validation for Loop breaker and lifecycle limits.

Deterministic failure precedence

  • Records every failed attempt and its chosen disposition before taking the next action.
  • Applies retry, authored error routing, absorption, and generation repair in a single deterministic order.
  • Adds attempt-suffixed identities, epoch fencing, compare-and-swap output writes, due-retry scanning, deadlines, and timeout classification.
  • Prevents a stale attempt from overwriting progress produced by a newer attempt or generation.

Durable effects

  • Expands effects into an outbox inside the same transaction as the lifecycle decision.
  • Delivers emit and daemon-tool effects through an isolated relay with stable delivery keys.
  • Pre-binds effect context, builds Loop links, sanitizes payloads, and records render/delivery errors as data without rolling back the owning lifecycle transition.
  • Makes relay replay safe across daemon crashes and restarts.

Target health and quarantine

  • Adds Loop-target health tracking and breaker admission before dispatch.
  • Converts unavailable targets into classified failures that enter the same precedence chain.
  • Quarantines only the affected node lane while the rest of the run remains truthful and observable.
  • Persists quarantine entries and supports requeue succession with explicit origin and attention state.

Storage and migrations

  • Extends the declarative SQLite schema and generated sqlc layer for attempts, effects, lifecycle controls, waits/admission state, and target health.
  • Appends new Goose migrations after the current main history; no existing migration bytes or Atlas history are rewritten.
  • Preserves fresh-create, reopen, ahead-version, integrity, and schema-equivalence invariants.

Runtime safety invariants

  • One committed disposition per attempt.
  • One durable effect identity per trigger and subject.
  • No stale epoch may overwrite newer progress.
  • A breaker or quarantine decision is scoped to the target/node lane, not the workspace.
  • Restart replay is idempotent and derives from committed state.
  • Error-route work does not inherit the failed source's success dependency.

Verification

  • Migration code generation and drift checks passed during the rebase onto current main.
  • Runtime unit, integration, and race-enabled suites were added in their owning packages.
  • The final make gate-full is 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

  • Native tools: no native tool IDs land in this layer; runtime data and lifecycle operations are introduced for feat: add Loop lifecycle controls and surfaces (stack 2/3) #301 to expose. Checked builtin registration and schema generation boundaries.
  • Extensibility and hooks: adds durable effect/outbox execution and lifecycle event families while preserving existing extension IDs, bridge SDKs, MCP sidecars, and hook registration boundaries. Config lifecycle adds Loop breaker/lifecycle defaults and validation.
  • Workspace data isolation: all new run, node, attempt, effect, wait, admission, health, and quarantine data is workspace/run scoped. Store queries and coordinator paths carry workspace identity and reject cross-workspace reads or mutation.
  • Official Compozy skill: no public surface changes in this layer; the bundled skill update ships in feat: complete Loop lifecycle Web, docs, and QA (stack 3/3) #302 together with the public CLI/native contract from feat: add Loop lifecycle controls and surfaces (stack 2/3) #301.

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

    • Added configurable loop lifecycle controls for retries, deadlines, waits, autopause rules, admission, and circuit breakers.
    • Added durable retry scheduling, node quarantine and requeue workflows, cancellation states, and wait handling.
    • Added lifecycle effects with reliable delivery, approvals, event emission, and idempotent processing.
    • Added failure classification, retry and repair feedback, target health protection, result contracts, error routing, and lifecycle validation.
  • Bug Fixes

    • Improved timeout handling, stale-schedule protection, late-arrival tracking, and recovery of due retries.
    • Improved configuration handling for unsigned 64-bit values and clearer validation diagnostics.

pedronauck and others added 4 commits August 3, 2026 18:47
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]>
@greptile-apps

greptile-apps Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Too many files changed for review. (212 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@vercel

vercel Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
compozy-site Ready Ready Preview Aug 3, 2026 11:41pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0fc40751-7982-489a-a669-a08bbc1cc29c

📥 Commits

Reviewing files that changed from the base of the PR and between e41e4c1 and f535e0f.

📒 Files selected for processing (5)
  • internal/config/tool_surface_test.go
  • internal/loop/coordinator_lifecycle.go
  • internal/loop/coordinator_lifecycle_test.go
  • internal/loop/coordinator_test.go
  • internal/loop/target_health_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/loop/coordinator_lifecycle.go
  • internal/loop/target_health_test.go
  • internal/config/tool_surface_test.go
  • internal/loop/coordinator_lifecycle_test.go
  • internal/loop/coordinator_test.go

Walkthrough

This 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.

Changes

Loop lifecycle configuration

Layer / File(s) Summary
Lifecycle configuration and validation
internal/config/*, internal/loop/lifecycle_config.go
Adds lifecycle defaults, layered resolution, validation, autopause rules, breaker settings, and CLI configuration paths.
Lifecycle DSL and linting
internal/loop/dsl/*, internal/loop/linter_*
Adds lifecycle, wait, effect, retry, result-contract, and terminal-state DSL support with grammar validation and diagnostics.

Coordinator execution and daemon runtime

Layer / File(s) Summary
Retry, failure, quarantine, and requeue flow
internal/loop/coordinator_*, internal/loop/failure_*, internal/loop/node_*
Adds attempt tracking, retry scheduling, failure classification, error routing, parked dependencies, quarantine, requeue succession, and repair feedback.
Target health and authored timeouts
internal/loop/target_health.go, internal/loop/coordinator_target_health.go, internal/daemon/loop_action_*, internal/daemon/loop_target_health.go
Adds loop-target admission, breaker transitions, authored timeout reasons, and target-health runtime wiring.
Effect rendering and relay
internal/loop/effect_*.go, internal/loop/effects_dispatch.go, internal/daemon/loop_effect_relay*.go
Adds effect context and rendering, persisted delivery intents, daemon-side tool and event delivery, acknowledgement handling, replay, and shutdown management.

Durable storage and scheduling

Layer / File(s) Summary
Snapshots and lifecycle persistence
internal/loop/generation_snapshot*.go, internal/store/globaldb/global_db_loop*.go
Persists attempts, controls, lifecycle events, retry metadata, output epochs, cancellation state, late arrivals, requeues, and effect acknowledgements.
Retry recovery and schema support
internal/daemon/scheduler_loop_*, internal/store/globaldb/schema/*, internal/store/globaldb/queries/*, internal/store/globaldb/sqlcgen/*
Adds timer and due-scan retry recovery, stale-schedule fencing, lifecycle tables, indexes, migrations, SQL queries, and generated models.

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

Possibly related PRs

  • compozy/compozy#290: Extends durable loop lifecycle and event semantics in related code.
  • compozy/compozy#295: Shares loop lifecycle implementation changes across the same runtime and storage surfaces.
  • compozy/compozy#301: Directly relates to loop lifecycle implementation, including retry, wait, effects, and event configuration.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% 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 PR's main change: adding the first stack of Loop lifecycle runtime foundations.
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 stack/loop-lifecycle-v2-01-runtime

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 3, 2026 •

Copy link
Copy Markdown
Contributor

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit f535e0f.

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

Wrap 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.Fatalf in 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 failureClass and nextAttemptAt values 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 win

Add the canceled terminal outcome to this coverage test.

dsl.TerminalCanceled, dsl.TerminalEffects.OnCanceled, and EffectTriggerOnCanceled exist, but TestCoordinatorEffectsShouldAttachEveryDeclaredTerminalOutcome still only covers the six earlier terminal statuses. Add the canceled case 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 win

Assert 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 win

Keep a non-empty code and recovery in the failure output.

Two gaps exist in this segment:

  • Line 58 assigns code from reasonErr.Code without an empty fallback. The ToolError branch at Lines 67-70 does guard this case. If reasonErr.Code is empty or whitespace, NewActionFailure receives an empty code, while marshalLoopActionFailureMetadata keeps loopActionFailureCode because 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 win

Wrap the test body in a t.Run("Should ...") subtest.

The test body runs directly inside TestBootDeadEntityRegistryShouldIsolateLoopTargetPolicy without a t.Run("Should ...") subtest. Other test files in this PR, such as loop_action_failure_test.go, use the t.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 win

Advance the retry scan cursor only after a successful page.

EnqueueDueLoopRetryWakesPage returns a zero-value cursor when its list query fails, but enqueueDueLoopRetryWakes stores 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 win

Add 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 ActionRepairFailure with 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 win

Enforce retry deadlines when FirstScheduledAt is nil.

planNodeRetry has a configured deadline but skips the cutoff for any output without FirstScheduledAt, so scheduled retry backoff can resume after the node’s total deadline. Require a persisted first-scheduled timestamp for deadline-bearing cells, or use input.now as 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 win

Handle transient tool delivery failures without closing to EffectFailed.

effectOutboxStateForOutcome maps every non-OK result to EffectFailed, but loopEffectRelay.effectToolError can produce tool-call errors such as context deadline exceeded. These rows keep state = 'failed', so the dispatcher never reclaims them in DrainPendingLoopEffects; only attempts changes. Either keep transient tool failures in pending with a retry cap or add a retryable failure state and a retry schedule.

Also, delivered_at is set for failed tool calls. Use last_attempt_at or a separate closed_at column if delivered_at should 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 win

Surface missing coordinator timer armer wiring.

coordinatorTimerArmer is left nil when newLoopRetryTimerArmer returns nil, and armCoordinatorTimers still returns nil for non-zero plan.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 win

Two 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: add t.Parallel() as the first statement of TestCoordinatorRunnerShouldKeepHealthyLaneRunningWhenTargetIsUnavailable.
  • internal/loop/coordinator_test.go#L2426-L2428: add t.Parallel() as the first statement of TestCoordinatorRunnerShouldIsolateRepeatedFailures.

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 win

Define and implement retention for loop_admission_claims.

loop_admission_claims is only present in the schema and integration test. The admission claim persists when its loop_run_id or workspace_id later 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 win

Report an absent current epoch instead of zero.

state.epoch is a sql.NullInt64 filled by a LEFT JOIN. When the output row no longer exists, Valid is false and state.epoch.Int64 is 0. The diagnostic payload then reports current_epoch: 0, which reads as a real epoch. Epochs start at 1, per validateRetryDueCell. Emit nil when 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 win

Normalize the trimmed identity, or reject untrimmed values.

ValidateNodeRequeueMutation trims WorkspaceID, RunID, and NodeID only for the emptiness check. The mutation is a value, so the trimmed values are discarded. A caller that passes " looprun-1 " passes validation, and internal/store/globaldb/global_db_loop_node_requeue.go then binds the untrimmed value in its loop_node_controls lookup. The request fails later with ErrInvalidTransition or ErrRunNotFound instead 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 win

Reject a non-positive generation in the attempt ledger.

validate only compares a.Generation with the caller-supplied generation. If the caller passes 0, the identity check passes and a row with generation = 0 becomes 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

📥 Commits

Reviewing files that changed from the base of the PR and between dc65b22 and 845a795.

⛔ Files ignored due to path filters (21)
  • docs/qa/bugs/BUG-20260802-loop-lifecycle-config-unsupported.md is excluded by !**/*.md
  • docs/qa/charters/CH-loop-lifecycle-config-cli.md is excluded by !**/*.md
  • docs/qa/charters/CH-loop-quarantine-repair.md is excluded by !**/*.md
  • docs/qa/journeys/J-bound-runaway-work.md is excluded by !**/*.md
  • docs/qa/journeys/J-recover-loop-node-failure.md is excluded by !**/*.md
  • docs/qa/journeys/J-tune-loop-lifecycle-defaults.md is excluded by !**/*.md
  • docs/qa/reports/2026-08-02-loop-lifecycle-config.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-error-route-fallback.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-loop-lifecycle-config-cli.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-on-error-notification-with-context.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-quarantine-diagnose-requeue.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-sick-target-degrades-one-lane.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-terminal-outcome-notification.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-transient-blip-heals.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-unannotated-escalation.md is excluded by !**/*.md
  • docs/qa/scenarios/TA-loop-failure-breaker.md is excluded by !**/*.md
  • internal/store/globaldb/schema/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
  • internal/testutil/acpmock/testdata/loop_node_lifecycle_fixture.json is excluded by !**/*.json
  • openapi/compozy.json is excluded by !**/*.json
  • web/src/generated/compozy-openapi.d.ts is excluded by !**/generated/**, !**/generated/**, !**/*.d.ts
  • web/src/generated/loop-enums.ts is excluded by !**/generated/**, !**/generated/**
📒 Files selected for processing (181)
  • internal/api/contract/loop_enums.go
  • internal/api/core/loops_test.go
  • internal/cli/config_path_classification.go
  • internal/cli/config_test.go
  • internal/config/config.go
  • internal/config/loop_autopause.go
  • internal/config/loop_lifecycle.go
  • internal/config/loops.go
  • internal/config/loops_test.go
  • internal/config/merge_loops.go
  • internal/config/tool_surface_loops.go
  • internal/config/tool_surface_test.go
  • internal/daemon/boot.go
  • internal/daemon/boot_components.go
  • internal/daemon/boot_dead_entity.go
  • internal/daemon/boot_publish.go
  • internal/daemon/daemon.go
  • internal/daemon/daemon_shutdown_runtime.go
  • internal/daemon/daemon_shutdown_targets.go
  • internal/daemon/loop_action_failure.go
  • internal/daemon/loop_action_failure_test.go
  • internal/daemon/loop_action_liveness.go
  • internal/daemon/loop_action_runtime.go
  • internal/daemon/loop_defaults.go
  • internal/daemon/loop_defaults_test.go
  • internal/daemon/loop_effect_relay.go
  • internal/daemon/loop_effect_relay_boot.go
  • internal/daemon/loop_effect_relay_test.go
  • internal/daemon/loop_goal_command_definition.go
  • internal/daemon/loop_goal_executor_test.go
  • internal/daemon/loop_goal_managed_runtime_integration_test.go
  • internal/daemon/loop_node_lifecycle_e2e_integration_test.go
  • internal/daemon/loop_target_health.go
  • internal/daemon/loop_target_health_test.go
  • internal/daemon/scheduler_loop_coordinator.go
  • internal/daemon/scheduler_loop_due_scan.go
  • internal/daemon/scheduler_loop_retry_timer.go
  • internal/daemon/scheduler_runtime.go
  • internal/daemon/scheduler_runtime_test.go
  • internal/daemon/scheduler_task_source_types.go
  • internal/daemon/settings_runtime_applier.go
  • internal/daemon/task_runtime_boot.go
  • internal/daemon/task_runtime_boot_roles.go
  • internal/daemon/task_runtime_test.go
  • internal/deadentity/service_test.go
  • internal/loop/action_failure.go
  • internal/loop/action_repair_feedback.go
  • internal/loop/action_retry_feedback.go
  • internal/loop/action_runagent.go
  • internal/loop/action_test.go
  • internal/loop/action_types.go
  • internal/loop/admission_claim.go
  • internal/loop/api_types.go
  • internal/loop/cancellation.go
  • internal/loop/config.go
  • internal/loop/constants.go
  • internal/loop/control_namespace.go
  • internal/loop/control_namespace_history.go
  • internal/loop/control_namespace_history_test.go
  • internal/loop/control_namespace_test.go
  • internal/loop/control_plan.go
  • internal/loop/control_plan_finish.go
  • internal/loop/control_readiness.go
  • internal/loop/control_tasks.go
  • internal/loop/coordinator.go
  • internal/loop/coordinator_action.go
  • internal/loop/coordinator_effects.go
  • internal/loop/coordinator_error_routes.go
  • internal/loop/coordinator_generation.go
  • internal/loop/coordinator_generation_idle.go
  • internal/loop/coordinator_generation_reattempt.go
  • internal/loop/coordinator_generation_succession.go
  • internal/loop/coordinator_goal_control_test.go
  • internal/loop/coordinator_history.go
  • internal/loop/coordinator_lifecycle.go
  • internal/loop/coordinator_lifecycle_test.go
  • internal/loop/coordinator_metadata.go
  • internal/loop/coordinator_options.go
  • internal/loop/coordinator_outputs.go
  • internal/loop/coordinator_quarantine.go
  • internal/loop/coordinator_requeue.go
  • internal/loop/coordinator_retry.go
  • internal/loop/coordinator_retry_test.go
  • internal/loop/coordinator_snapshot_test.go
  • internal/loop/coordinator_succession.go
  • internal/loop/coordinator_target_health.go
  • internal/loop/coordinator_terminal_helpers.go
  • internal/loop/coordinator_test.go
  • internal/loop/dsl/codec_test.go
  • internal/loop/dsl/contract.go
  • internal/loop/dsl/effects.go
  • internal/loop/dsl/graph.go
  • internal/loop/dsl/lifecycle.go
  • internal/loop/dsl/node_params.go
  • internal/loop/dsl/refs/condition.go
  • internal/loop/dsl/refs/diagnostics.go
  • internal/loop/dsl/refs/namespace.go
  • internal/loop/dsl/refs/refs_test.go
  • internal/loop/dsl/types_nodes.go
  • internal/loop/dsl/wait.go
  • internal/loop/effect_context.go
  • internal/loop/effect_outbox.go
  • internal/loop/effects_dispatch.go
  • internal/loop/effects_dispatch_test.go
  • internal/loop/errors.go
  • internal/loop/executed_definition_snapshot.go
  • internal/loop/failure_class.go
  • internal/loop/failure_classify.go
  • internal/loop/failure_classify_test.go
  • internal/loop/failure_repair.go
  • internal/loop/failure_repair_test.go
  • internal/loop/failure_result_contract.go
  • internal/loop/failure_result_contract_test.go
  • internal/loop/generation_intent.go
  • internal/loop/generation_snapshot.go
  • internal/loop/generation_snapshot_attempts.go
  • internal/loop/generation_snapshot_controls.go
  • internal/loop/generation_snapshot_intents.go
  • internal/loop/generation_snapshot_test.go
  • internal/loop/lifecycle_config.go
  • internal/loop/linter_contract.go
  • internal/loop/linter_effects.go
  • internal/loop/linter_lifecycle.go
  • internal/loop/linter_test.go
  • internal/loop/linter_wait.go
  • internal/loop/node_attempt.go
  • internal/loop/node_control.go
  • internal/loop/node_control_mutation.go
  • internal/loop/node_requeue.go
  • internal/loop/node_wait.go
  • internal/loop/predicate_policy.go
  • internal/loop/predicate_policy_test.go
  • internal/loop/quarantine.go
  • internal/loop/retry_due.go
  • internal/loop/service_test.go
  • internal/loop/service_types.go
  • internal/loop/target_health.go
  • internal/loop/target_health_test.go
  • internal/loop/types.go
  • internal/loop/watch_events_registry_phase_a.go
  • internal/store/globaldb/global_db_goal_reentry_test.go
  • internal/store/globaldb/global_db_goal_turn_runtime_integration_test.go
  • internal/store/globaldb/global_db_loop.go
  • internal/store/globaldb/global_db_loop_admission_claims.go
  • internal/store/globaldb/global_db_loop_effect_outbox.go
  • internal/store/globaldb/global_db_loop_events.go
  • internal/store/globaldb/global_db_loop_node_attempts.go
  • internal/store/globaldb/global_db_loop_node_controls.go
  • internal/store/globaldb/global_db_loop_node_requeue.go
  • internal/store/globaldb/global_db_loop_node_terminal.go
  • internal/store/globaldb/global_db_loop_node_waits.go
  • internal/store/globaldb/global_db_loop_retry_due.go
  • internal/store/globaldb/global_db_loop_scan.go
  • internal/store/globaldb/global_db_loop_schema_integration_test.go
  • internal/store/globaldb/global_db_loop_test.go
  • internal/store/globaldb/global_db_task_claim_complete.go
  • internal/store/globaldb/global_db_task_claim_test.go
  • internal/store/globaldb/global_db_task_coordinator_boundaries.go
  • internal/store/globaldb/global_db_task_coordinator_effects.go
  • internal/store/globaldb/global_db_task_coordinator_intents.go
  • internal/store/globaldb/global_db_task_coordinator_runtime.go
  • internal/store/globaldb/loop_sqlc_mapping.go
  • internal/store/globaldb/queries/loop_core.sql
  • internal/store/globaldb/queries/loop_lifecycle.sql
  • internal/store/globaldb/queries/loop_runtime.sql
  • internal/store/globaldb/queries/task_claim.sql
  • internal/store/globaldb/schema/definitions/41_reliability.sql
  • internal/store/globaldb/schema/definitions/50_loops.sql
  • internal/store/globaldb/schema/migrations/00043_schema.sql
  • internal/store/globaldb/schema/migrations/00044_schema.sql
  • internal/store/globaldb/sqlcgen/loop_core.sql.go
  • internal/store/globaldb/sqlcgen/loop_lifecycle.sql.go
  • internal/store/globaldb/sqlcgen/loop_runtime.sql.go
  • internal/store/globaldb/sqlcgen/models.go
  • internal/store/globaldb/sqlcgen/task_claim.sql.go
  • internal/store/types_dead_entity.go
  • internal/task/coordinator.go
  • internal/task/coordinator_timer.go
  • internal/task/manager.go
  • internal/task/manager_constructor.go
  • internal/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

Comment thread internal/cli/config_test.go
Comment thread internal/config/tool_surface_loops.go Outdated
Comment thread internal/daemon/loop_action_runtime.go
Comment thread internal/daemon/loop_effect_relay.go
Comment thread internal/loop/coordinator_effects.go
Comment thread internal/loop/target_health.go
Comment thread internal/loop/target_health.go
Comment thread internal/store/globaldb/global_db_loop_effect_outbox.go Outdated
Comment thread internal/store/globaldb/global_db_task_claim_complete.go Outdated
Comment thread internal/store/globaldb/global_db_task_claim_test.go
@pedronauck

Copy link
Copy Markdown
Member Author

CodeRabbit round 1 was remediated in e41e4c1.

  • Fixed all 30 findings that apply to this stack layer, including the 17 inline threads now resolved.
  • The canceled-terminal effect suggestion is intentionally deferred: Task 03 does not introduce a canceled Loop terminal, and Task 05 owns that cross-surface contract.
  • Transient effect delivery is intentionally not retried: Task 03 and ADR-015 define one close-or-record attempt per delivery, retaining failures as evidence instead of adding an implicit retry protocol.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
internal/loop/coordinator_lifecycle_test.go (1)

272-276: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Set now on the hand-built runner.

This runner leaves now nil. The test passes only because GetTaskRun fails before applyTerminalNodeLifecycle calls r.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

📥 Commits

Reviewing files that changed from the base of the PR and between 845a795 and e41e4c1.

⛔ Files ignored due to path filters (2)
  • .codex/scripts/coderabbit-review-graphql.sh is excluded by !.codex/**
  • internal/store/globaldb/schema/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (58)
  • internal/cli/config.go
  • internal/cli/config_path_classification.go
  • internal/cli/config_roles.go
  • internal/cli/config_test.go
  • internal/cli/config_value_parse.go
  • internal/config/tool_surface.go
  • internal/config/tool_surface_coercion.go
  • internal/config/tool_surface_loops.go
  • internal/config/tool_surface_test.go
  • internal/config/tool_surface_types.go
  • internal/daemon/loop_action_failure.go
  • internal/daemon/loop_action_failure_test.go
  • internal/daemon/loop_action_runtime.go
  • internal/daemon/loop_effect_relay.go
  • internal/daemon/loop_effect_relay_test.go
  • internal/daemon/loop_target_health_test.go
  • internal/daemon/scheduler_loop_due_scan.go
  • internal/daemon/scheduler_runtime_test.go
  • internal/daemon/task_runtime_test.go
  • internal/loop/action_repair_feedback.go
  • internal/loop/action_test.go
  • internal/loop/admission_claim.go
  • internal/loop/coordinator_effects.go
  • internal/loop/coordinator_error_routes.go
  • internal/loop/coordinator_lifecycle.go
  • internal/loop/coordinator_lifecycle_test.go
  • internal/loop/coordinator_outputs.go
  • internal/loop/coordinator_requeue.go
  • internal/loop/coordinator_retry.go
  • internal/loop/coordinator_retry_test.go
  • internal/loop/coordinator_target_health.go
  • internal/loop/coordinator_test.go
  • internal/loop/dsl/graph.go
  • internal/loop/dsl/types_test.go
  • internal/loop/effect_context.go
  • internal/loop/effects_dispatch_test.go
  • internal/loop/generation_snapshot_attempts.go
  • internal/loop/generation_snapshot_intents.go
  • internal/loop/generation_snapshot_test.go
  • internal/loop/node_attempt.go
  • internal/loop/node_requeue.go
  • internal/loop/target_health.go
  • internal/loop/target_health_test.go
  • internal/store/globaldb/global_db_loop_admission_claims.go
  • internal/store/globaldb/global_db_loop_effect_outbox.go
  • internal/store/globaldb/global_db_loop_node_requeue.go
  • internal/store/globaldb/global_db_loop_retry_due.go
  • internal/store/globaldb/global_db_loop_schema_integration_test.go
  • internal/store/globaldb/global_db_loop_test.go
  • internal/store/globaldb/global_db_task_claim_complete.go
  • internal/store/globaldb/global_db_task_claim_test.go
  • internal/store/globaldb/queries/loop_lifecycle.sql
  • internal/store/globaldb/schema/definitions/50_loops.sql
  • internal/store/globaldb/schema/migrations/00045_schema.sql
  • internal/store/globaldb/sqlcgen/loop_lifecycle.sql.go
  • internal/store/globaldb/sqlcgen/models.go
  • internal/task/manager_coordinator_run.go
  • internal/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

Comment thread internal/config/tool_surface_test.go Outdated
Comment thread internal/loop/coordinator_lifecycle.go
Comment thread internal/loop/coordinator_test.go
Comment thread internal/loop/target_health_test.go

This branch was successfully deployed

1 active deployment
Preview — f535e0f5 Deployed Aug 3, 2026 by vercel[bot]
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.

1 participant