Skip to content

feat: complete Loop lifecycle Web, docs, and QA (stack 3/3) - #302

Closed
pedronauck wants to merge 10 commits into
stack/loop-lifecycle-v2-02-controls-surfacesfrom
stack/loop-lifecycle-v2-03-web-docs-qa
Closed

pedronauck wants to merge 10 commits into
stack/loop-lifecycle-v2-02-controls-surfacesfrom
stack/loop-lifecycle-v2-03-web-docs-qa

Conversation

@pedronauck

@pedronauck pedronauck commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Summary

This is the top and final PR in native GitHub stack #303. It completes the Loop node lifecycle feature end to end: runtime failure policy, long-running supervision, cancel/kill/pause/wait controls, agent-manageable public surfaces, Web authoring and operations, public documentation, bundled skill guidance, and production-like QA.

The stack replaces #294, whose 657-file review surface exceeded CodeRabbit's 300-file selection limit. Each layer now remains below that limit under the repository's .coderabbit.yaml filters.

Stack and review order

Order Pull request Scope Base GitHub files CodeRabbit-visible
1 #300 Runtime foundations: schema, classification, precedence, effects, health, quarantine main 202 184
2 #301 Liveness, controls, waits, HTTP/UDS/CLI/native contract #300 300 253
3 #302 (this PR) Web, editor, docs, official skill, QA and final fixes #301 316 227

Review and merge from the bottom upward. GitHub's stack merge creates one ordered squash commit per PR; it does not create one aggregate commit for the full stack.

Why this feature exists

Loop nodes previously had an incomplete lifecycle model: failures lacked one durable precedence contract, long-running agent work could be confused with dead work, operator cancellation was not distinct from immediate termination, durable waits and quarantine were not manageable across every public surface, and the Web UI could not truthfully author or operate the full runtime grammar.

This feature makes the daemon the single source of truth for every state and transition. The same committed facts now drive coordinator recovery, HTTP/UDS, CLI, native tools, SSE/read models, Web controls, documentation, and QA.

Complete feature scope

1. Failure classification and diagnostics

  • Defines a closed failure-class vocabulary with ordered evidence and remediation hints.
  • Detects explicit action errors and payload-declared failures through built-in rules and authored result contracts.
  • Separates authoring diagnostics from runtime failures so invalid definitions fail before execution.
  • Sanitizes and carries useful recovery context into retry, route, repair, quarantine, events, and effects.

2. One deterministic failure precedence

  • Commits the attempt and disposition before acting.
  • Applies policy in one order: eligible retry → authored error route → explicit absorption → generation repair/escalation.
  • Adds bounded backoff, retry-after support, timeouts/deadlines, due scanning, attempt identities, and epoch fences.
  • Prevents stale attempts or coordinator snapshots from overwriting newer progress.
  • Ensures error-route targets do not inherit success dependencies from the failed source.

3. Durable reactions and effects

  • Expands effects transactionally into an outbox with stable delivery identity.
  • Relays custom events and daemon tool calls with per-entry isolation and crash-safe replay.
  • Supplies pre-bound effect context, Loop/run/node links, outcome facts, and sanitized failure details.
  • Records rendering or delivery errors as data without rolling back the lifecycle transition that requested the reaction.

4. Long-running supervision

  • Allows legitimate days-long work without an implicit wall-clock death sentence.
  • Treats progress, silence, session liveness, and target health as separate signals.
  • Resumes provider/session death from committed progress under bounded policy.
  • Preserves a wake that arrives while a coordinator snapshot is in flight.
  • Parks watch-event nodes at zero cost and replays ledger cursor gaps after restart exactly once.

5. Cancel, kill, pause, and resume

  • Cooperative cancel is durable and drains to canceled(operator_cancel).
  • Immediate kill stops the bound session and ends at canceled(operator_kill).
  • Run/node controls are idempotent, race-safe, and return deterministic structured winner information.
  • Pause supports drain and immediate modes; resume supports normal, immediate, and reset-attempt modes.
  • The obsolete Loop stop verb and operator_stop cause are deleted instead of aliased.

6. Durable waits and admission dedupe

  • Persists timer, approval, and watch-event waits without holding a lease while parked.
  • Reconciles due waits and escalations after daemon restart.
  • Claims watch-event admission atomically with expiry and delivery deduplication.
  • Exposes truthful waiting inventories, subscriptions, cursors, escalation facts, and last-wake time.

7. Target health, quarantine, and requeue

  • Checks target health before dispatch and feeds unavailable targets into the normal failure chain.
  • Tracks breaker state independently for each Loop target.
  • Quarantines only the affected node lane while the run continues where possible.
  • Stores the classified attempt chain and remediation hint used by the operator UI.
  • Requeues through a new succession with explicit origin rather than mutating history.

8. Agent and operator manageability

  • Co-ships lifecycle operations through HTTP, UDS, compozy loop, OpenAPI, generated TypeScript, and eight compozy__loop_* native tools.
  • Uses the same validators and writers across non-UI and Web surfaces.
  • Includes tool schemas, descriptor digests, risk flags, capability gates, dependency diagnostics, and deterministic conflict payloads.
  • Keeps all reads and mutations scoped to the registered public workspace identity.

9. Web operations and truthful state

  • Adds all 12 run states to filters, pills, activity/past grouping, detail read models, and reduced-motion behavior.
  • Adds run/node pause, resume, cancel, kill, quarantine, and requeue controls with confirmation and conflict answers.
  • Adds waiting and quarantine inventories, classified attempt history, effect context, and explicit next-action guidance.
  • Keeps controls hidden when the daemon says a transition is unavailable; optimistic UI never invents a successful transition.

10. Loop authoring and hero path

  • Extends the editor grammar for retry, error routing, effects, waits, watch-event subscriptions, time bounds, and lifecycle policy.
  • Adds lifecycle-aware inspector forms, diagnostics, canvas states, and hard-navigation support.
  • Completes the catalog → required-input run form → live run detail path.
  • Uses daemon-generated contract data and the shared @compozy/ui primitives/tokens rather than parallel Web-only schemas or controls.

11. Documentation and bundled skill

  • Documents lifecycle concepts, failure handling, guardrails, cancel versus kill, pause/resume, waits, quarantine, configuration, CLI, API, and native tools.
  • Updates the official skills/compozy/ guidance so agents can discover and operate the public lifecycle contract.
  • Aligns all active QA and public wording to 12 states and removes stale Loop stop language while leaving dated historical reports unchanged.

Storage and rebase notes

  • The copied feature history was rebased onto current main; the original loop-node-lifecycle branch and closed feat: loop node lifecycle #294 remain preserved.
  • Main and the feature had concurrent migration numbers. Existing main migration bytes and Atlas history were preserved exactly; the feature migrations were appended after them.
  • The Loop-run table rebuild preserves main's automation watch triggers by dropping and recreating them around the rebuild.
  • The admission-claim migration preserves the feature's explicit expiry backfill instead of adding an unsafe bare NOT NULL column.
  • OpenAPI, TypeScript, sqlc, Atlas, and generated docs were regenerated from canonical sources after conflict resolution.

QA evidence

The production-like run is recorded in docs/qa/reports/2026-08-03-loop-node-lifecycle.md.

  • 23 scenarios walked: 13 pass/fixed, 10 blocked-verify, 0 untested, 0 fail.
  • Personas covered operator, managing agent, Loop author, and mobile approver paths.
  • Surfaces covered CLI, HTTP/UDS, native tools, Web, daemon restart, approvals, repair, quarantine, requeue, cancel, kill, error routes, and effects.
  • Five product bugs were fixed in production and re-walked; no test expectation was weakened.
  • Blocked verifications require public controls that do not yet exist for clock advance, event replay, provider death, or synthetic breaker state; the report names each exact gap and available automated evidence.
  • The isolated lab teardown recorded clean: true; no QA daemon, tmux server, browser, watcher, or dev server was left alive.

Additional focused evidence collected during rebase:

  • internal/tools/builtin: race-enabled canonical suite, 270 tests passed.
  • Daemon workspace access and hosted-session diagnostics: 8 integration matrix cases passed.
  • Code generation/drift checks passed after migration and contract resolution.
  • Frontend lint, typecheck, 532 Vitest files / 4,289 tests, and Web build passed in the latest full-gate attempts.
  • Go lint findings were reduced to formatting/constant issues and corrected; a targeted golines run now passes.

Gate status

The final make gate-full was intentionally stopped before completion to start CodeRabbit review now. Per the delivery decision, the full gate will run again after all CodeRabbit findings are remediated and before merge. This description does not claim a final green full gate.

Breaking changes and delete targets

  • Deletes the former Loop stop run/node verbs, HTTP/UDS routes, CLI/native entries, schemas, UI controls, and operator_stop transition cause.
  • Uses cancel for cooperative drain and kill for immediate termination; there is no compatibility alias or dual field.
  • Adds watching as a live run state and canceled as its own truthful terminal state, bringing the public vocabulary to 12 states.
  • Updates runtime, storage, APIs, CLI, native tools, Web, docs, official skill, specs, and QA in the same stack.

Compozy Impact Audit

  • Native tools: adds the Loop lifecycle native tool family and updates builtin descriptors, schemas, digests, risk flags, availability diagnostics, capability gates, tests, and CLI/HTTP fallbacks. Retired stop IDs/behavior are absent.
  • Extensibility and hooks: adds lifecycle event/effect integration, durable outbox delivery, watch-event wake behavior, and authored error-route/effect grammar. Existing extension IDs, bridge SDKs, bundles, MCP sidecars, and unrelated registries are unchanged. Loop breaker/lifecycle config defaults, validation, schema, and docs are co-shipped.
  • Workspace data isolation: new data is workspace/run/node/attempt scoped. Workspace identity flows through CLI, HTTP, UDS, core, stores, SSE/events, Web queries, native tools, and diagnostics. Neighbor reads and foreign lifecycle mutations are rejected; output payload lookup is scoped by workspace, run, generation, node, item, and reference.
  • Official Compozy skill: updates skills/compozy/ for the new CLI/native lifecycle paths, state vocabulary, cancel/kill semantics, waits, quarantine, and agent-operable recovery flows.

Review guide

  1. Review feat: add Loop lifecycle runtime foundations (stack 1/3) #300 first for storage, transaction boundaries, failure precedence, replay identity, and quarantine scope.
  2. Review feat: add Loop lifecycle controls and surfaces (stack 2/3) #301 next for liveness, cancel/kill race semantics, durable waits, and public-surface parity.
  3. Review this PR for Web truthfulness, editor grammar, generated contract consumption, docs/skill alignment, and QA evidence.
  4. CodeRabbit-visible counts are computed from .coderabbit.yaml; Markdown, generated docs, fixtures, Web E2E, and other configured exclusions explain the difference from GitHub's raw file totals.

Merge plan

After CodeRabbit findings are fixed and make gate-full passes on the final tree, merge the stack bottom-to-top using GitHub's squash strategy. The expected result is three ordered squash commits, one per PR layer.

Stack created with the official GitHub Stacks CLI.

Summary by CodeRabbit

  • New Features

    • Added lifecycle-aware loop editing for retries, waits, deadlines, effects, error handling, and terminal reactions.
    • Added loop-node inventory with filtering, sorting, pagination, and lifecycle details.
    • Added pause, resume, cancel, kill, requeue, quarantine, and wait-resume controls for runs and nodes.
    • Improved run pages with waiting, attention, retry, quarantine, and cancellation status views.
    • Added clearer catalog grouping, reliability summaries, run-limit summaries, and lifecycle indicators.
    • Added read-only editing with fork-to-edit support and clearer publish validation feedback.
  • Bug Fixes

    • Improved large-output handling, lifecycle conflict reporting, workspace identification, and cancellation behavior.

@greptile-apps

greptile-apps Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Too many files changed for review. (323 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 4, 2026 1:54am

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: c84b8438-27af-4b1d-8582-cb04671e1543

📥 Commits

Reviewing files that changed from the base of the PR and between b067b70 and f542f2a.

⛔ Files ignored due to path filters (1)
  • .codex/agents/explorer.toml is excluded by !**/*.toml, !.codex/**
📒 Files selected for processing (8)
  • internal/config/tool_surface_coercion.go
  • internal/daemon/loop_action_liveness_policy.go
  • internal/daemon/loop_action_runtime.go
  • internal/loop/coordinator_autopause.go
  • internal/loop/effects_dispatch_test.go
  • internal/loop/generation_snapshot_intent_collections.go
  • internal/loop/generation_snapshot_intents.go
  • internal/store/globaldb/global_db_task_claim_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/store/globaldb/global_db_task_claim_test.go

Walkthrough

This PR adds loop lifecycle contracts, structured schemas, externalized generation-output hydration, fenced action bindings, coordinator safeguards, lifecycle-aware loop editing, run controls, node inventory, quarantine views, and related tests.

Changes

Loop platform lifecycle changes

Layer / File(s) Summary
Lifecycle contracts and API boundaries
internal/api/contract/*, internal/api/spec/*, internal/api/core/*, internal/daemon/*
Adds typed lifecycle contracts, structured schemas, workspace-ID corrections, scheduler lease handling, and lifecycle request tests.
Runtime output materialization
internal/loop/*, internal/store/globaldb/*
Adds content-addressed output storage and scoped hydration. Runtime consumers resolve external payloads before reading output values.
Binding ownership and coordinator progress
internal/loop/action_*, internal/store/globaldb/*
Adds shared action keys, cell fences, activation ownership checks, stale-binding cleanup, deterministic coordinator wake reservation, and idempotent coordinator runs.
Lifecycle authoring and validation
web/src/systems/loops/lib/*, web/src/systems/loops/components/editor/*, web/src/systems/loops/hooks/*
Adds lifecycle schema fields, effect and wait editors, atomic draft updates, read-only forking, warning counters, and publish-rejection details.
Lifecycle run views and controls
web/src/systems/loops/components/run-page/*, web/src/systems/os/apps/loops/*
Adds lifecycle projections, parked progress, story rows, node and run controls, quarantine views, waiting and attention panels, and cancellation-cause rendering.
Catalog, inventory, and shared UI
web/src/systems/loops/components/catalog/*, web/src/systems/loops/components/detail/*, web/src/systems/loops/components/runs/*, packages/ui/*
Adds grouped catalog presentation, reliability and rail sections, node inventory, run-form guidance, and dialog body content.

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

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant LoopRunPage
  participant useLoopNodeControls
  participant loopNodesAPI
  participant LoopRepo
  Operator->>LoopRunPage: Select node action
  LoopRunPage->>useLoopNodeControls: Open confirmation
  useLoopNodeControls->>loopNodesAPI: Submit lifecycle mutation
  loopNodesAPI->>LoopRepo: Persist node lifecycle mutation
  LoopRepo-->>loopNodesAPI: Return mutation result or conflict details
  loopNodesAPI-->>useLoopNodeControls: Return typed result or conflict error
  useLoopNodeControls-->>LoopRunPage: Refresh lifecycle data and show outcome
Loading

Possibly related PRs

  • compozy/compozy#122: Modifies related loop lifecycle and web UI contracts, schemas, API handlers, and lifecycle components.
  • compozy/compozy#290: Modifies related generation-history projections and externalized output handling.
  • compozy/compozy#301: Introduces related loop lifecycle control APIs and state models.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.96% 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 accurately summarizes the pull request's completion of Loop lifecycle support across Web, documentation, and QA.
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-03-web-docs-qa

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 1 new issue in 1 file · 1 error · score 84 / 100 (Needs work) · 0 fixed · vs main

Errors

Reviewed by React Doctor for commit f542f2a. See inline comments for fixes.

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (18)
web/src/systems/loops/components/run-page/loop-run-waits-rail.tsx-22-42 (1)

22-42: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the "Waiting on you" row: missing nodeId and possible label mismatch.

Two issues in this block:

  1. "Needs attention" and "Quarantined" attach the single node's id when their count is 1 (lines 35, 40). "Waiting on you" does not, even when waiting === 1, though waiting is computed the same way. This is an inconsistency in the row-building logic.
  2. waiting counts every node with any open wait, including timer waits. In loop-run-parked-panels.tsx, the timer wait copy explicitly states the wait "resumes by itself" — it does not need the user. Labeling this count "Waiting on you" implies user action is needed even for self-resuming timer waits.

As per path instructions, "never imply unsupported metrics, controls, states, or repair paths" for web/**/*.{tsx,md} files. Confirm whether the label should be relabeled (e.g., "Open waits") or the count should be scoped to decision-needed waits only.

🛠️ Proposed fix for the missing `nodeId`
   const rows: { label: string; value: number; nodeId?: string }[] = [
-    { label: "Waiting on you", value: waiting },
+    {
+      label: "Waiting on you",
+      value: waiting,
+      nodeId: waiting === 1 ? nodes.find(node => node.waits.some(isOpenWait))?.nodeId : undefined,
+    },
🤖 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 `@web/src/systems/loops/components/run-page/loop-run-waits-rail.tsx` around
lines 22 - 42, Update the row-building logic around isOpenWait so the “Waiting
on you” metric does not imply user action for self-resuming timer waits: either
scope the count to decision-needed waits or relabel it to “Open waits” if all
open waits remain included. Add the matching single-node nodeId lookup to that
row, consistent with the “Needs attention” and “Quarantined” rows.

Source: Path instructions

web/src/systems/loops/components/stories/loop-run-lifecycle-fixtures.ts-205-221 (1)

205-221: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align age_seconds with created_at in the wait fixtures.

The wait() factory sets created_at: minutesAgo(18). Two overrides change age_seconds but keep that default. The timer wait declares 360 seconds (6 minutes) and the approval wait declares 22 minutes. A component that renders created_at and a component that renders age_seconds will then show different ages in the same capture. Override created_at together with age_seconds.

🐛 Proposed fix
       wait({
         node_id: "collect_fixes",
         kind: "timer",
         resume_at: minutesAgo(-24),
+        created_at: minutesAgo(6),
         age_seconds: 360,
       }),
       wait({ node_id: "fix_batch", item_index: 3, kind: "event", age_seconds: 18 * 60 }),
       wait({
         node_id: "finalize_round",
         kind: "approval_escalation",
         claim_state: "intervention_required",
         escalation_cursor: 1,
+        created_at: minutesAgo(22),
         age_seconds: 22 * 60,
         next_escalation_at: minutesAgo(-8),
       }),
🤖 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 `@web/src/systems/loops/components/stories/loop-run-lifecycle-fixtures.ts`
around lines 205 - 221, Update the timer wait and approval_escalation wait
entries in the waits fixture to override created_at alongside their age_seconds
values, using timestamps that represent 6 minutes and 22 minutes ago
respectively. Keep the existing wait factory defaults and all other fixture
fields unchanged.
web/src/systems/loops/lib/loop-node-now-view.ts-110-118 (1)

110-118: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard the formatted time before adding the at clause.

clockTime returns "" when requested_at is absent or unparsable. The template at line 114 always produces a non-empty string, so the .filter(Boolean) at line 116 cannot remove it. The provenance line then ends with the dangling text at. Build the clause only when a time is available. The same pattern applies to line 154.

🐛 Proposed fix
 function pausedLine(node: LoopNodeLifecycle): LoopNodeNowLine {
   const provenance = node.pauseProvenance;
   const actor = provenance ? actorLabel(provenance.actor_kind, provenance.actor_id) : "";
   const byRule = provenance?.rule_id ? `rule ${provenance.rule_id}` : "";
   const by = byRule || actor;
+  const requestedAt = provenance ? clockTime(provenance.requested_at) : "";
   return {
     nodeId: node.nodeId,
     state: "paused",
     headline: `${node.label} is paused`,
     body: `${by === "" ? "Parked" : `Paused by ${by}`} at a safe point — the rest of the run keeps working.`,
     chip: null,
     provenance: provenance
       ? [
           `paused by ${by || "unknown"}`,
           provenance.reason ? `reason “${provenance.reason}”` : null,
-          `at ${clockTime(provenance.requested_at)}`,
+          requestedAt === "" ? null : `at ${requestedAt}`,
         ]
           .filter(Boolean)
           .join(" · ")
       : null,
🤖 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 `@web/src/systems/loops/lib/loop-node-now-view.ts` around lines 110 - 118,
Update the provenance formatting in the visible block and the corresponding
block around the second occurrence to call clockTime(provenance.requested_at)
once, then add the “at” clause only when that formatted value is non-empty;
preserve the existing filtering and joining behavior so no dangling “at” remains
for missing or unparsable timestamps.
web/src/systems/loops/components/run-page/loop-quarantine-sheet.tsx-222-233 (1)

222-233: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add unit/e2e coverage for the requeue gating rule.

The LoopQuarantineSheet only uses node.quarantined for the loop-quarantine-requeue button, and existing Stories/rg output do not show a canonical assertion for the non-quarantined branch. A test should cover both quarantined and already-out-of-quarantine states so refactors do not expose a rejected requeue action.

🤖 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 `@web/src/systems/loops/components/run-page/loop-quarantine-sheet.tsx` around
lines 222 - 233, Add unit or end-to-end coverage for the requeue control in
LoopQuarantineSheet, asserting that loop-quarantine-requeue is available for
quarantined nodes and absent or unavailable for nodes where node.quarantined is
false; preserve the existing isRequeuePending disabled behavior.
web/src/systems/loops/lib/loop-reliability.ts-60-73 (1)

60-73: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not present the first node limits as one loop-wide policy.

firstDefined can select timeout from one node and deadline from another node. The resulting text can describe a time budget that no node has.

Collect limits by node. If values differ, identify the affected nodes or state that only some steps use each limit.

🤖 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 `@web/src/systems/loops/lib/loop-reliability.ts` around lines 60 - 73, Update
deadlineTile to aggregate timeout and deadline values per node instead of
selecting independent firstDefined values across graph.nodes. When limits differ
or apply only to some steps, describe the affected nodes or explicitly indicate
partial coverage, ensuring the generated clauses and codes never imply a
loop-wide policy that no node has.
web/src/systems/loops/lib/loop-graph.ts-101-104 (1)

101-104: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Read wait expiry from params.expires.

Wait controls store their configuration in params. The projection reads root-level expires, so an authored expiry never reaches waitTile. The test fixture repeats this invalid shape and hides the defect.

  • web/src/systems/loops/lib/loop-graph.ts#L101-L104: read after from record.params.expires.
  • web/src/systems/loops/lib/__tests__/loop-reliability.test.ts#L50-L59: create the wait expiry under params and retain the expiry assertion.
🤖 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 `@web/src/systems/loops/lib/loop-graph.ts` around lines 101 - 104, Read wait
expiry from record.params.expires in the loop-graph projection so authored
values reach waitTile. Update web/src/systems/loops/lib/loop-graph.ts lines
101-104 accordingly; in
web/src/systems/loops/lib/__tests__/loop-reliability.test.ts lines 50-59, move
the fixture expiry under params and retain the existing expiry assertion.
web/src/systems/loops/components/editor/loop-editor-publish-rejected-strip.tsx-34-39 (1)

34-39: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Show the version-kept status independently of the issue list, and do not hardcode 422.

The badge renders only when issues.length > 0. A rejection that carries a message but no issue list then hides the statement that the compare-and-swap version was kept, which is the main point stated in the component docstring. The literal 422 also assumes the status code; a rejection returned with another status displays a wrong code. Pass the status as a prop, or drop the code from the text.

🩹 Proposed change
       <p className="flex items-center gap-2 text-form-label text-danger">
         <AlertCircle aria-hidden="true" className="size-3.5 shrink-0" />
         {message}
-        {issues.length > 0 ? (
-          <span className="ml-auto font-mono text-mono-id text-subtle">
-            422 · expected_version v{version ?? "?"} kept
-          </span>
-        ) : null}
+        <span className="ml-auto font-mono text-mono-id text-subtle">
+          expected_version v{version ?? "?"} kept
+        </span>
       </p>
🤖 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
`@web/src/systems/loops/components/editor/loop-editor-publish-rejected-strip.tsx`
around lines 34 - 39, Update the version-kept badge in the component rendering
around `issues` so it renders independently of `issues.length`, including when a
rejection has a message but no issues. Remove the hardcoded `422` by either
accepting and rendering the actual rejection status through the component props
or omitting the status code from the badge text.
web/src/systems/loops/lib/loop-runs-view.ts-174-177 (1)

174-177: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the fallback order for kind and reference.

kind prefers started_origin_kind, while ref prefers started_by_ref. When a run carries both pairs and they differ, the line pairs the origin kind with the actor reference, for example schedule · pedro. That reads as provenance the daemon never recorded. Pick one pair first and fall back to the other pair as a unit.

🩹 Proposed fix
-  const kind = run.started_origin_kind || run.started_by_kind || "";
-  const ref = run.started_by_ref || run.started_origin_ref || "";
+  const originKind = run.started_origin_kind || "";
+  const originRef = run.started_origin_ref || "";
+  const kind = originKind || run.started_by_kind || "";
+  const ref = originKind ? originRef : run.started_by_ref || "";
   if (kind && ref) return `${kind} · ${ref}`;
   return kind || ref || "—";
🤖 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 `@web/src/systems/loops/lib/loop-runs-view.ts` around lines 174 - 177, Update
the run label construction so the kind and reference values are selected from
the same provenance pair: prefer started_origin_kind with started_origin_ref,
then fall back together to started_by_kind with started_by_ref. Preserve the
existing formatting and em-dash fallback when no complete pair is available.
web/src/systems/loops/lib/loop-start-kinds.ts-13-17 (1)

13-17: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Expose thisFormDeclared in the run-form UI or remove it.

LoopRunWaysToStart only displays thisForm and alsoDeclared, while every run post is always http. Add kind/http to this form when thisFormDeclared is false, or remove the unused field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/systems/loops/lib/loop-start-kinds.ts` around lines 13 - 17, Update
LoopRunWaysToStart to use thisFormDeclared: when the current run form is not
declared, include the http kind in the displayed run-start options; otherwise
preserve the existing thisForm and alsoDeclared behavior. Alternatively, remove
thisFormDeclared from the relevant type and producer if it is not needed.
web/src/systems/loops/hooks/use-loop-node-inventory.ts-43-43 (1)

43-43: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use query.isLoading for the inventory skeleton state.

When no inventory state is selected, the query is disabled and v5 keeps status: "pending" with fetchStatus: "idle", so query.isPending is true while no request runs. Consumers that render a loading view from isLoading can show it permanently in that state; use query.isLoading, which is isPending && isFetching.

🐛 Proposed fix
-    isLoading: query.isPending,
+    isLoading: query.isLoading,
🤖 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 `@web/src/systems/loops/hooks/use-loop-node-inventory.ts` at line 43, Update
the inventory query state mapping in useLoopNodeInventory to expose
query.isLoading instead of query.isPending for isLoading, preserving the
intended skeleton behavior when the disabled query is pending without fetching.
web/src/systems/loops/components/run-page/loop-run-control-dialog.tsx-17-18 (1)

17-18: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The error prop is declared but the only consumer does not pass it.

The doc for error states that the daemon's rejection is shown inline. In web/src/systems/os/apps/loops/loop-run-detail-location.tsx, LoopRunControlDialog receives generation, isPending, onConfirm, onOpenChange, runId, status, and verb, but no error. LoopNodeControlDialog in the same file does receive error={nodeControls.answer}. A rejected run cancel or kill therefore shows no inline answer, unlike the node path.

Pass the run action's rejection text into error at the consumer, or remove the prop if the run path reports failures elsewhere.

Also applies to: 50-50

🤖 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 `@web/src/systems/loops/components/run-page/loop-run-control-dialog.tsx` around
lines 17 - 18, Update the LoopRunControlDialog invocation in
loop-run-detail-location.tsx to pass the run action’s rejection text through its
existing error prop, matching LoopNodeControlDialog’s use of
nodeControls.answer. Preserve the current dialog behavior and use the run
control state’s corresponding answer/error symbol.
web/src/systems/loops/lib/loop-node-verb-copy.ts-124-134 (1)

124-134: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not state an episode number when the payload carries no quarantine entry.

(node.quarantineEntry?.episodes.length ?? 1) + 1 renders "episode 2" when quarantineEntry is absent. The payload never reported an episode in that case. loopNodeStateStrip uses ?? 0 for the same field and omits the episode when it is zero, so the two helpers disagree. The file header states that a missing detail must shorten the sentence rather than be invented.

Drop the episode sentence when the entry is absent.

🐛 Proposed fix
-  requeue: node => ({
-    eyebrow: "Requeue node",
-    title: `Send ${node.nodeId} back to the queue?`,
-    body: `The lane leaves quarantine and runs again through the normal path, with all limits applying. If it fails again, it returns here as episode ${
-      (node.quarantineEntry?.episodes.length ?? 1) + 1
-    }.`,
+  requeue: node => {
+    const episodes = node.quarantineEntry?.episodes.length;
+    const nextEpisode =
+      episodes === undefined ? "" : ` If it fails again, it returns here as episode ${episodes + 1}.`;
+    return {
+      eyebrow: "Requeue node",
+      title: `Send ${node.nodeId} back to the queue?`,
+      body: `The lane leaves quarantine and runs again through the normal path, with all limits applying.${nextEpisode}`,
       confirmLabel: "Requeue lane",
       cancelLabel: "Keep quarantined",
       tone: "accent",
       micro: "node_requeued · origin requeue · with provenance",
-  }),
+    };
+  },

As per coding guidelines: "never imply unsupported metrics, controls, states, or repair paths".

🤖 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 `@web/src/systems/loops/lib/loop-node-verb-copy.ts` around lines 124 - 134,
Update the requeue confirmation builder to omit the episode sentence when
node.quarantineEntry is absent, rather than defaulting its count to produce an
invented episode number. Preserve the existing episode calculation and wording
when quarantineEntry is present, and follow the shortening behavior used by
loopNodeStateStrip.

Source: Coding guidelines

web/src/systems/loops/lib/loop-run-story-lifecycle-rows.ts-176-186 (1)

176-186: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Separate the raw cause from the following sentence.

cause comes straight from the payload's failure.cause and is usually a machine token, for example connection_refused. The template joins it with a single space before a capitalized sentence, so the row reads "connection_refused Its clock is suspended…". Add an explicit separator so the two parts do not read as one sentence.

🐛 Proposed fix
     sub:
-      `${cause === "" ? "" : `${cause} `}` +
+      `${cause === "" ? "" : `${cause} — `}` +
       "Its clock is suspended and no attempts are being spent; the rest of the run keeps going.",
🤖 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 `@web/src/systems/loops/lib/loop-run-story-lifecycle-rows.ts` around lines 176
- 186, Update the node_quarantined row construction to explicitly separate the
raw cause from the following sentence in the sub text. Adjust the cause prefix
logic near the node_quarantined return so non-empty causes receive appropriate
punctuation or delimiter, while preserving the no-cause output and existing
sentence text.
web/src/systems/loops/components/run-page/loop-node-row-actions.tsx-39-52 (1)

39-52: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the requeue icon when the promoted verb is requeue.

The promoted button always renders Play. LoopNodeControlMenu renders RotateCcw for the same requeue verb. The same action therefore shows two different icons in the same row.

🐛 Proposed fix to match the menu icon
-import { Play } from "lucide-react";
+import { Play, RotateCcw } from "lucide-react";
       {primary ? (
         <Button
           className="min-h-6"
           data-testid={`loop-node-primary-${primary}-${node.nodeId}`}
           disabled={isPending}
           onClick={() => onVerb(primary, node)}
           size="sm"
           type="button"
           variant="outline"
         >
-          <Play aria-hidden="true" className="size-3" />
-          {primary === "resume" ? "Resume" : "Requeue…"}
+          {primary === "resume" ? (
+            <Play aria-hidden="true" className="size-3" />
+          ) : (
+            <RotateCcw aria-hidden="true" className="size-3" />
+          )}
+          {primary === "resume" ? "Resume" : "Requeue…"}
         </Button>
       ) : null}
🤖 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 `@web/src/systems/loops/components/run-page/loop-node-row-actions.tsx` around
lines 39 - 52, Update the primary action button in the loop-node row actions to
render the same icon as LoopNodeControlMenu for each promoted verb: keep Play
for resume and use RotateCcw for requeue. Preserve the existing button behavior,
labels, and accessibility attributes.
web/src/systems/loops/lib/loop-run-progress.ts-140-153 (1)

140-153: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Derive the parked noun from the fan-out node, not from every parked node.

parkedNoun scans all lifecycle nodes. The parked segments belong to one node only, the widest fan-out node. If that node is paused while an unrelated node is quarantined, states.size === 2 and the meta line says "parked" instead of "paused". latestFanOut already resolves widestId, so return it and read that node's state.

♻️ Proposed fix
 interface FanOutBranches {
   /** Branch statuses ordered by ascending `item_index`. */
   statuses: string[];
   /** True when the node owning these branches is parked by the daemon. */
   parked: boolean;
+  /** The node that owns these branches. */
+  nodeId: string;
 }
-  return { statuses, parked: widestId !== null && parked.has(widestId) };
+  return { statuses, parked: widestId !== null && parked.has(widestId), nodeId: widestId ?? "" };
-function parkedNoun(nodes: readonly LoopNodeLifecycle[]): string {
-  const states = new Set<LoopNodeLifecycle["state"]>();
-  for (const node of nodes) {
-    if (node.parked) states.add(node.state);
-  }
-  if (states.size === 1) {
-    const [only] = [...states];
-    if (only === "paused") return "paused";
-    if (only === "quarantined") return "quarantined";
-    if (only === "waiting") return "waiting";
-  }
-  return "parked";
-}
+function parkedNoun(nodes: readonly LoopNodeLifecycle[], nodeId: string): string {
+  const state = nodes.find(node => node.parked && node.nodeId === nodeId)?.state;
+  if (state === "paused" || state === "quarantined" || state === "waiting") return state;
+  return "parked";
+}
-  const noun = parkedNoun(nodes);
+  const noun = parkedNoun(nodes, fanOut.nodeId);

Also applies to: 192-200

🤖 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 `@web/src/systems/loops/lib/loop-run-progress.ts` around lines 140 - 153,
Update parkedNoun to derive the noun from the node identified by latestFanOut’s
resolved widestId, rather than aggregating states across all parked lifecycle
nodes. Read that fan-out node’s state and return paused, quarantined, or waiting
when applicable, otherwise return parked; preserve the existing fallback
behavior when the node or state is unavailable.
web/src/systems/loops/lib/loop-node-inventory.ts-73-108 (1)

73-108: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Format the timestamps before showing them to operators.

waitReason prints wait.resume_at and retryReason prints output.next_attempt_at directly. These fields are raw API timestamps, so the row reads like timer — resumes 2026-08-03T14:00:00Z. Reuse the existing time formatting helper used elsewhere in the loops system, or express the value as a relative duration.

🤖 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 `@web/src/systems/loops/lib/loop-node-inventory.ts` around lines 73 - 108,
Update waitReason and retryReason to format wait.resume_at and
output.next_attempt_at through the existing loops-system time formatting helper
before displaying them. Preserve the current message structure and fallback
behavior, but ensure operators see a human-readable or relative timestamp
instead of raw API ISO values.
web/src/systems/loops/components/run-page/loop-run-outcome-card.tsx-172-178 (1)

172-178: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat an empty cause as absent.

cause ?? failure?.code keeps an empty string. If the terminal frame carries cause: "", causeLabel becomes "", the segment is dropped, and the available failure.code is never shown. Use a truthiness fallback.

🐛 Proposed fix
-  const causeLabel = cause ?? failure?.code;
+  const causeLabel = cause || failure?.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 `@web/src/systems/loops/components/run-page/loop-run-outcome-card.tsx` around
lines 172 - 178, Update the causeLabel assignment in the loop-run outcome card
to use a truthiness fallback, so an empty cause is treated as absent and
failure?.code is used instead. Preserve the existing precedence for non-empty
causes and the subsequent micro segment behavior.
web/src/systems/os/apps/loops/loop-runs-location.tsx-128-147 (1)

128-147: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Source loopOptions from the loop catalog before building the inventory view.

LoopNodeInventoryViewProps.loopOptions says the filter comes from the workspace catalog, but this passes only loop names from loaded runs. If runsQuery loads first but the inventory query is still pending, the node inventory renders with no loop options. Use the catalog query for this option list, or rename the prop contract to “loop names seen in the loaded runs.”

🤖 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 `@web/src/systems/os/apps/loops/loop-runs-location.tsx` around lines 128 - 147,
Update the loopOptions value passed to LoopNodeInventoryView to source loop
names from the workspace loop catalog query rather than runs, ensuring options
remain available while inventory data is pending. Preserve the existing sorting
and deduplication behavior, and use the catalog query’s established data symbol.
🧹 Nitpick comments (11)
web/src/systems/loops/components/run-page/loop-run-story-timeline.tsx (1)

65-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use distinct icons for "retry" and "requeued".

retry and requeued both map to RotateCcw. These are different lifecycle concepts: a scheduled automatic retry and an actor-initiated requeue. The companion type file documents a "one fixed lucide icon per concept" rule. Reusing the same icon for two different concepts makes them indistinguishable in the timeline.

Assign a separate icon to one of the two states.

As per coding guidelines, packages/ui/**/*-adjacent web conventions call for distinguishable, consistent iconography per concept; the in-file comment states: "One fixed lucide icon per concept across the domain, per DESIGN-LESSONS L7."

🤖 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 `@web/src/systems/loops/components/run-page/loop-run-story-timeline.tsx` around
lines 65 - 76, Update the icon mapping so the retry and requeued states use
distinct Lucide icons, preserving the existing icon for one state and assigning
an appropriate unused icon to the other. Keep the one-fixed-icon-per-concept
convention in the status mapping.
internal/loop/coordinator_wait.go (1)

226-227: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider reusing generationOutputRefForPayload here.

The code sets the reference, the runtime payload, and later appends a GenerationOutputBlob (Line 245) by hand. generationOutputRefForPayload(payload, outputBlobs, at) already performs the reference and blob steps in one place, as used in internal/loop/coordinator_watch_events.go Line 281. Reusing it keeps one owner for externalized payload persistence and prevents future drift, for example a missed blob append.

The current order is correct, because setGenerationOutputRef clears runtimePayload.

🤖 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_wait.go` around lines 226 - 227, Replace the manual
payload reference and blob handling in the coordinator wait flow with
generationOutputRefForPayload, passing payload, outputBlobs, and the existing at
value. Remove the redundant setGenerationOutputRef, runtimePayload assignment,
and later GenerationOutputBlob append while preserving the current ordering and
resulting output.
internal/loop/generation_output_runtime.go (1)

34-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Accept a typed reader instead of any.

generationOutputRuntimeView takes reader any and asserts the interface at runtime. A typed parameter moves the contract to compile time and removes the assertion branch. Callers that hold a store value can pass it directly; the function can still reject a nil reader when a content-addressed reference exists.

Also consider renaming generationOutputRuntimeRef: it returns a payload document when runtimePayload is set, not a reference.

As per coding guidelines: "Define interfaces where they are consumed, use direct interface calls".

♻️ Proposed typing change
 func generationOutputRuntimeView(
 	ctx context.Context,
-	reader any,
+	reader GenerationOutputPayloadReader,
 	scope generationOutputRuntimeScope,
 	outputs []GenerationOutput,
 ) ([]GenerationOutput, error) {
 	view := cloneGenerationOutputs(outputs)
 	if len(view) == 0 {
 		return view, nil
 	}
-	var payloadReader GenerationOutputPayloadReader
 	resolved := make(map[GenerationOutputPayloadKey]json.RawMessage)
 	for index := range view {
 		ref := strings.TrimSpace(view[index].OutputRef)
 		if !OutputRefLooksContentAddressed(ref) {
 			continue
 		}
-		if payloadReader == nil {
-			var ok bool
-			payloadReader, ok = reader.(GenerationOutputPayloadReader)
-			if !ok || payloadReader == nil {
-				return nil, fmt.Errorf("%w: generation output payload reader is required", ErrValidation)
-			}
-		}
+		if reader == nil {
+			return nil, fmt.Errorf("%w: generation output payload reader is required", ErrValidation)
+		}
🤖 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/generation_output_runtime.go` around lines 34 - 57, Change
generationOutputRuntimeView to accept GenerationOutputPayloadReader directly
instead of reader any, remove the runtime type assertion and related
payloadReader initialization, and call the typed reader interface directly when
resolving content-addressed outputs. Preserve validation by rejecting a nil
reader only when such a reference requires payload access. Rename
generationOutputRuntimeRef to reflect that it returns a payload document when
runtimePayload is set, and update all call sites.

Source: Coding guidelines

web/src/systems/loops/lib/loop-node-schema-types.ts (1)

97-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Export one shared WAIT_MODES list next to LoopWaitMode.

The same vocabulary is declared twice as a value: web/src/systems/loops/lib/loop-node-wait-fields.ts Line 14 and web/src/systems/loops/components/editor/loop-editor-wait-mode.tsx Line 15. If the DSL adds a fourth wait mode, one list can be updated and the other missed. Declare the list once and derive the type from it, then import the list in both consumers.

♻️ Proposed single source of truth
-/** The closed `wait` discriminator vocabulary (`dsl.WaitParams`). */
-export type LoopWaitMode = "for" | "until" | "event";
+/** The closed `wait` discriminator vocabulary (`dsl.WaitParams`). */
+export const WAIT_MODES = ["for", "until", "event"] as const;
+export type LoopWaitMode = (typeof WAIT_MODES)[number];
🤖 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 `@web/src/systems/loops/lib/loop-node-schema-types.ts` around lines 97 - 98,
Define and export a shared WAIT_MODES constant beside LoopWaitMode, deriving
LoopWaitMode from its literal values instead of maintaining a separate union.
Update loop-node-wait-fields.ts and loop-editor-wait-mode.tsx to import and
reuse WAIT_MODES, removing their duplicated wait-mode value lists.
internal/loop/generation_snapshot.go (1)

44-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document that runtimePayload is not serialized.

ExpectedEpoch carries a comment that explains its non-durable role. runtimePayload has no comment. encoding/json ignores unexported fields, so this payload is dropped on every snapshot marshal or unmarshal. Add a short comment that states the field is runtime-only and is hydrated in-package, so future readers do not expect it to survive a store round-trip.

📝 Proposed comment
 	ExpectedEpoch  *int64 `json:"expected_epoch,omitempty"`
+	// runtimePayload holds the hydrated output payload for in-process reads only.
+	// It is intentionally unexported and never serialized into the snapshot.
 	runtimePayload json.RawMessage
🤖 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/generation_snapshot.go` around lines 44 - 47, Add a concise
comment above runtimePayload stating that it is runtime-only, is not serialized
by encoding/json, and is hydrated in-package rather than preserved across
snapshot store round-trips.
web/src/systems/loops/components/stories/loop-run-scenario-props.ts (1)

39-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicate nodeLifecycles assignment.

Line 40 spreads view, which already contains nodeLifecycles. Line 49 sets the same key again from the same source. Keeping both creates two places to change if the source ever differs.

♻️ Proposed change
     workspaceLabel: "Home",
     versionLabel: `v${run.definition_version} · pinned`,
-    nodeLifecycles: view.nodeLifecycles,
     nodeNowLines: buildNodeNowLines(view.nodeLifecycles, view.graph, live.retrySchedules),
🤖 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 `@web/src/systems/loops/components/stories/loop-run-scenario-props.ts` around
lines 39 - 51, Remove the explicit nodeLifecycles property from the returned
object in the scenario props builder, leaving the value supplied by the ...view
spread while preserving the derived nodesById and nodeNowLines fields.
internal/loop/goal/types_binding.go (1)

139-154: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

State the mutual exclusion on CellFence in the contract.

validateBindingActivationOwner rejects a request that sets CellFence together with CheckpointKey, ExpectedControlEpoch, or GrantID. That rule is only visible in the store package. Add a doc comment on CellFence that names the exclusive alternative, so callers learn the constraint from the contract type.

📝 Proposed comment
 type ActivateBindingRequest struct {
 	Key                  BindingKey
 	CheckpointKey        *TurnKey
+	// CellFence activates from an ordinary loop cell. When it is set, the Goal
+	// owner fields (CheckpointKey, ExpectedControlEpoch, GrantID) must be empty.
 	CellFence            *BindingCellFence
🤖 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/goal/types_binding.go` around lines 139 - 154, Add a doc
comment to the CellFence field in ActivateBindingRequest stating that it is
mutually exclusive with CheckpointKey, ExpectedControlEpoch, and GrantID,
matching the validation in validateBindingActivationOwner.
web/src/systems/loops/components/run-page/loop-node-control-menu.tsx (1)

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

Consider one source of truth for "destructive".

Two definitions now describe destructive verbs. DESTRUCTIVE_VERBS here controls placement below the separator and includes cancel and kill. LOOP_NODE_VERB_PRESENTATION controls the danger styling and marks only kill as destructive: true. The result is intentional today, but a future edit to one list will not update the other.

Derive the placement set from the presentation map, or rename the local set to state that it controls order only.

♻️ Suggested rename to separate placement from styling
-/** Destructive verbs sit below a separator so they are never the reflex click. */
-const DESTRUCTIVE_VERBS = new Set<LoopNodeVerb>(["cancel", "kill"]);
+/**
+ * Verbs that end work. They sit below a separator so they are never the reflex
+ * click. Danger styling is separate and comes from
+ * `LOOP_NODE_VERB_PRESENTATION[verb].destructive`.
+ */
+const STOPPING_VERBS = new Set<LoopNodeVerb>(["cancel", "kill"]);

Also applies to: 112-112

🤖 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 `@web/src/systems/loops/components/run-page/loop-node-control-menu.tsx` at line
52, Clarify the purpose of DESTRUCTIVE_VERBS in the loop-node control menu by
renaming it to indicate that it controls menu ordering or separator placement,
not danger styling. Update its references while preserving the existing
cancel/kill placement behavior and the separate LOOP_NODE_VERB_PRESENTATION
styling definitions.
web/src/systems/loops/components/__tests__/loop-run-page.test.tsx (1)

546-571: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated LoopNodeLifecycle fixtures across the loops test suites. Both files hand-build the full lifecycle literal because no shared fixture factory exists, so every new lifecycle field needs an edit in each copy.

  • web/src/systems/loops/components/__tests__/loop-run-page.test.tsx#L546-L571: replace this factory and the two further literals in the file (Lines 646-670 and 738-762) with one shared factory that accepts overrides.
  • web/src/systems/loops/lib/__tests__/loop-run-progress.test.ts#L82-L108: import the same shared factory and pass state, paused, and quarantined as overrides.

As per path instructions: "Check for shared test utilities usage to avoid duplication".

🤖 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 `@web/src/systems/loops/components/__tests__/loop-run-page.test.tsx` around
lines 546 - 571, Create one shared LoopNodeLifecycle fixture factory, move the
duplicated defaults from loop-run-page.test.tsx into it, and replace all three
lifecycle literals in that file with factory calls using overrides. In
web/src/systems/loops/lib/__tests__/loop-run-progress.test.ts lines 82-108,
import the shared factory and pass state, paused, and quarantined through
overrides.

Source: Path instructions

packages/ui/src/components/__tests__/confirm-dialog.test.tsx (1)

188-192: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Guard the order assertions against missing elements.

indexOf returns -1 for an absent test id. If the note stops rendering, -1 < order.indexOf("confirm-body") still passes, so the test reports success for a broken dialog. Assert presence first.

♻️ Proposed assertion hardening
+    expect(order).toContain("confirm-note");
+    expect(order).toContain("confirm-body");
+    expect(order).toContain("cancel-action");
     expect(order.indexOf("confirm-note")).toBeLessThan(order.indexOf("confirm-body"));
     expect(order.indexOf("confirm-body")).toBeLessThan(order.indexOf("cancel-action"));
🤖 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 `@packages/ui/src/components/__tests__/confirm-dialog.test.tsx` around lines
188 - 192, Harden the order assertions in the confirm-dialog test by first
asserting that confirm-note, confirm-body, and cancel-action are present in the
queried test IDs. Then retain the existing relative-order checks so missing
elements cannot produce a false pass through indexOf returning -1.
web/src/systems/loops/components/editor/loop-editor-json-field.tsx (1)

24-37: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Note: this component does not resync text when value changes without a remount.

text is derived from value only at mount, through the lazy useState initializer. If a consumer changes value for the same component instance (no key change), the textarea keeps showing the old text. This is the mechanism behind the stale-JSON issue flagged in loop-editor-effects.tsx for the shape-switch flow.

The fix for that call site is a key={shape}. If a future call site reuses this component where the same instance can receive a different underlying value (for example, a JSON field bound to a node field that changes without a selectionKey remount), the same staleness will recur. Consider that when adding new call sites.

🤖 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 `@web/src/systems/loops/components/editor/loop-editor-json-field.tsx` around
lines 24 - 37, The current LoopEditorJsonField state initializes text only once,
so later value changes leave stale JSON displayed. At the
loop-editor-effects.tsx call site, force remounting by supplying key={shape};
when adding other call sites, also provide a key that changes whenever the
underlying value identity 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/daemon/scheduler_runtime_test.go`:
- Around line 366-421: Wrap the existing test body in
TestSchedulerTaskSourceLoopCoordinatorBackstopShouldDeferWhileConsumerLeaseIsActive
inside a t.Run subtest named with the established “Should …” pattern, while
preserving t.Parallel(), setup, and assertions.

In `@internal/loop/coordinator_succession.go`:
- Around line 151-160: Update loadPersistedRouteCauses to hydrate persisted
outputs with generationOutputRuntimeView before calling
generationOutputRuntimeRef, ensuring content-addressed references resolve to the
verdict JSON before json.Unmarshal. Apply this in the failure/reattempt path
while preserving the existing validation and error handling.

In `@web/src/systems/loops/components/editor/loop-editor-effects.tsx`:
- Around line 140-173: Add a React key to the LoopEditorJsonField instance based
on the current effect shape, such as shape, so switching between emit and tool
remounts the field and reinitializes its text from the updated value. Keep the
existing value, commit behavior, and other props unchanged.

In `@web/src/systems/loops/lib/loop-events.ts`:
- Around line 406-412: Guard frame-driven state updates in the switch, including
effect_results and node_retry_scheduled, with the same newer-sequence rule used
by retainFrame so replayed frames cannot mutate state. Ensure effectResults does
not append an existing deliveryId, and preserve the existing behavior for
genuinely newer frames.

In `@web/src/systems/loops/lib/loop-node-now-view.ts`:
- Around line 159-170: Update cancelingLine to use the normalized lifecycle
token "canceling" for the returned state instead of node.cancelState, while
preserving node.cancelState in the micro identity trail. Replace the raw cancel
state in the user-facing body text with plain-language cancellation copy that
does not expose wire values such as "draining" or "requested".

In `@web/src/systems/loops/lib/loop-quarantine-entry.ts`:
- Around line 175-191: The openedBy assignment in quarantineChainRows must match
retained episodes by generation rather than relying on the truncated array
index. For the first attempt of each non-initial episode, find the requeue whose
generation opens episode.generation and assign it; leave openedBy undefined when
no matching retained requeue exists.

---

Minor comments:
In
`@web/src/systems/loops/components/editor/loop-editor-publish-rejected-strip.tsx`:
- Around line 34-39: Update the version-kept badge in the component rendering
around `issues` so it renders independently of `issues.length`, including when a
rejection has a message but no issues. Remove the hardcoded `422` by either
accepting and rendering the actual rejection status through the component props
or omitting the status code from the badge text.

In `@web/src/systems/loops/components/run-page/loop-node-row-actions.tsx`:
- Around line 39-52: Update the primary action button in the loop-node row
actions to render the same icon as LoopNodeControlMenu for each promoted verb:
keep Play for resume and use RotateCcw for requeue. Preserve the existing button
behavior, labels, and accessibility attributes.

In `@web/src/systems/loops/components/run-page/loop-quarantine-sheet.tsx`:
- Around line 222-233: Add unit or end-to-end coverage for the requeue control
in LoopQuarantineSheet, asserting that loop-quarantine-requeue is available for
quarantined nodes and absent or unavailable for nodes where node.quarantined is
false; preserve the existing isRequeuePending disabled behavior.

In `@web/src/systems/loops/components/run-page/loop-run-control-dialog.tsx`:
- Around line 17-18: Update the LoopRunControlDialog invocation in
loop-run-detail-location.tsx to pass the run action’s rejection text through its
existing error prop, matching LoopNodeControlDialog’s use of
nodeControls.answer. Preserve the current dialog behavior and use the run
control state’s corresponding answer/error symbol.

In `@web/src/systems/loops/components/run-page/loop-run-outcome-card.tsx`:
- Around line 172-178: Update the causeLabel assignment in the loop-run outcome
card to use a truthiness fallback, so an empty cause is treated as absent and
failure?.code is used instead. Preserve the existing precedence for non-empty
causes and the subsequent micro segment behavior.

In `@web/src/systems/loops/components/run-page/loop-run-waits-rail.tsx`:
- Around line 22-42: Update the row-building logic around isOpenWait so the
“Waiting on you” metric does not imply user action for self-resuming timer
waits: either scope the count to decision-needed waits or relabel it to “Open
waits” if all open waits remain included. Add the matching single-node nodeId
lookup to that row, consistent with the “Needs attention” and “Quarantined”
rows.

In `@web/src/systems/loops/components/stories/loop-run-lifecycle-fixtures.ts`:
- Around line 205-221: Update the timer wait and approval_escalation wait
entries in the waits fixture to override created_at alongside their age_seconds
values, using timestamps that represent 6 minutes and 22 minutes ago
respectively. Keep the existing wait factory defaults and all other fixture
fields unchanged.

In `@web/src/systems/loops/hooks/use-loop-node-inventory.ts`:
- Line 43: Update the inventory query state mapping in useLoopNodeInventory to
expose query.isLoading instead of query.isPending for isLoading, preserving the
intended skeleton behavior when the disabled query is pending without fetching.

In `@web/src/systems/loops/lib/loop-graph.ts`:
- Around line 101-104: Read wait expiry from record.params.expires in the
loop-graph projection so authored values reach waitTile. Update
web/src/systems/loops/lib/loop-graph.ts lines 101-104 accordingly; in
web/src/systems/loops/lib/__tests__/loop-reliability.test.ts lines 50-59, move
the fixture expiry under params and retain the existing expiry assertion.

In `@web/src/systems/loops/lib/loop-node-inventory.ts`:
- Around line 73-108: Update waitReason and retryReason to format wait.resume_at
and output.next_attempt_at through the existing loops-system time formatting
helper before displaying them. Preserve the current message structure and
fallback behavior, but ensure operators see a human-readable or relative
timestamp instead of raw API ISO values.

In `@web/src/systems/loops/lib/loop-node-now-view.ts`:
- Around line 110-118: Update the provenance formatting in the visible block and
the corresponding block around the second occurrence to call
clockTime(provenance.requested_at) once, then add the “at” clause only when that
formatted value is non-empty; preserve the existing filtering and joining
behavior so no dangling “at” remains for missing or unparsable timestamps.

In `@web/src/systems/loops/lib/loop-node-verb-copy.ts`:
- Around line 124-134: Update the requeue confirmation builder to omit the
episode sentence when node.quarantineEntry is absent, rather than defaulting its
count to produce an invented episode number. Preserve the existing episode
calculation and wording when quarantineEntry is present, and follow the
shortening behavior used by loopNodeStateStrip.

In `@web/src/systems/loops/lib/loop-reliability.ts`:
- Around line 60-73: Update deadlineTile to aggregate timeout and deadline
values per node instead of selecting independent firstDefined values across
graph.nodes. When limits differ or apply only to some steps, describe the
affected nodes or explicitly indicate partial coverage, ensuring the generated
clauses and codes never imply a loop-wide policy that no node has.

In `@web/src/systems/loops/lib/loop-run-progress.ts`:
- Around line 140-153: Update parkedNoun to derive the noun from the node
identified by latestFanOut’s resolved widestId, rather than aggregating states
across all parked lifecycle nodes. Read that fan-out node’s state and return
paused, quarantined, or waiting when applicable, otherwise return parked;
preserve the existing fallback behavior when the node or state is unavailable.

In `@web/src/systems/loops/lib/loop-run-story-lifecycle-rows.ts`:
- Around line 176-186: Update the node_quarantined row construction to
explicitly separate the raw cause from the following sentence in the sub text.
Adjust the cause prefix logic near the node_quarantined return so non-empty
causes receive appropriate punctuation or delimiter, while preserving the
no-cause output and existing sentence text.

In `@web/src/systems/loops/lib/loop-runs-view.ts`:
- Around line 174-177: Update the run label construction so the kind and
reference values are selected from the same provenance pair: prefer
started_origin_kind with started_origin_ref, then fall back together to
started_by_kind with started_by_ref. Preserve the existing formatting and
em-dash fallback when no complete pair is available.

In `@web/src/systems/loops/lib/loop-start-kinds.ts`:
- Around line 13-17: Update LoopRunWaysToStart to use thisFormDeclared: when the
current run form is not declared, include the http kind in the displayed
run-start options; otherwise preserve the existing thisForm and alsoDeclared
behavior. Alternatively, remove thisFormDeclared from the relevant type and
producer if it is not needed.

In `@web/src/systems/os/apps/loops/loop-runs-location.tsx`:
- Around line 128-147: Update the loopOptions value passed to
LoopNodeInventoryView to source loop names from the workspace loop catalog query
rather than runs, ensuring options remain available while inventory data is
pending. Preserve the existing sorting and deduplication behavior, and use the
catalog query’s established data symbol.

---

Nitpick comments:
In `@internal/loop/coordinator_wait.go`:
- Around line 226-227: Replace the manual payload reference and blob handling in
the coordinator wait flow with generationOutputRefForPayload, passing payload,
outputBlobs, and the existing at value. Remove the redundant
setGenerationOutputRef, runtimePayload assignment, and later
GenerationOutputBlob append while preserving the current ordering and resulting
output.

In `@internal/loop/generation_output_runtime.go`:
- Around line 34-57: Change generationOutputRuntimeView to accept
GenerationOutputPayloadReader directly instead of reader any, remove the runtime
type assertion and related payloadReader initialization, and call the typed
reader interface directly when resolving content-addressed outputs. Preserve
validation by rejecting a nil reader only when such a reference requires payload
access. Rename generationOutputRuntimeRef to reflect that it returns a payload
document when runtimePayload is set, and update all call sites.

In `@internal/loop/generation_snapshot.go`:
- Around line 44-47: Add a concise comment above runtimePayload stating that it
is runtime-only, is not serialized by encoding/json, and is hydrated in-package
rather than preserved across snapshot store round-trips.

In `@internal/loop/goal/types_binding.go`:
- Around line 139-154: Add a doc comment to the CellFence field in
ActivateBindingRequest stating that it is mutually exclusive with CheckpointKey,
ExpectedControlEpoch, and GrantID, matching the validation in
validateBindingActivationOwner.

In `@packages/ui/src/components/__tests__/confirm-dialog.test.tsx`:
- Around line 188-192: Harden the order assertions in the confirm-dialog test by
first asserting that confirm-note, confirm-body, and cancel-action are present
in the queried test IDs. Then retain the existing relative-order checks so
missing elements cannot produce a false pass through indexOf returning -1.

In `@web/src/systems/loops/components/__tests__/loop-run-page.test.tsx`:
- Around line 546-571: Create one shared LoopNodeLifecycle fixture factory, move
the duplicated defaults from loop-run-page.test.tsx into it, and replace all
three lifecycle literals in that file with factory calls using overrides. In
web/src/systems/loops/lib/__tests__/loop-run-progress.test.ts lines 82-108,
import the shared factory and pass state, paused, and quarantined through
overrides.

In `@web/src/systems/loops/components/editor/loop-editor-json-field.tsx`:
- Around line 24-37: The current LoopEditorJsonField state initializes text only
once, so later value changes leave stale JSON displayed. At the
loop-editor-effects.tsx call site, force remounting by supplying key={shape};
when adding other call sites, also provide a key that changes whenever the
underlying value identity changes.

In `@web/src/systems/loops/components/run-page/loop-node-control-menu.tsx`:
- Line 52: Clarify the purpose of DESTRUCTIVE_VERBS in the loop-node control
menu by renaming it to indicate that it controls menu ordering or separator
placement, not danger styling. Update its references while preserving the
existing cancel/kill placement behavior and the separate
LOOP_NODE_VERB_PRESENTATION styling definitions.

In `@web/src/systems/loops/components/run-page/loop-run-story-timeline.tsx`:
- Around line 65-76: Update the icon mapping so the retry and requeued states
use distinct Lucide icons, preserving the existing icon for one state and
assigning an appropriate unused icon to the other. Keep the
one-fixed-icon-per-concept convention in the status mapping.

In `@web/src/systems/loops/components/stories/loop-run-scenario-props.ts`:
- Around line 39-51: Remove the explicit nodeLifecycles property from the
returned object in the scenario props builder, leaving the value supplied by the
...view spread while preserving the derived nodesById and nodeNowLines fields.

In `@web/src/systems/loops/lib/loop-node-schema-types.ts`:
- Around line 97-98: Define and export a shared WAIT_MODES constant beside
LoopWaitMode, deriving LoopWaitMode from its literal values instead of
maintaining a separate union. Update loop-node-wait-fields.ts and
loop-editor-wait-mode.tsx to import and reuse WAIT_MODES, removing their
duplicated wait-mode value lists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment on lines +366 to +421
func TestSchedulerTaskSourceLoopCoordinatorBackstopShouldDeferWhileConsumerLeaseIsActive(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t)
now := time.Date(2026, 8, 3, 22, 45, 0, 0, time.UTC)
workspaceID := "ws-backstop-active-consumer"
db := openDaemonTestGlobalDB(t)
if err := db.InsertWorkspace(ctx, workspacepkg.Workspace{
ID: workspaceID,
RootDir: t.TempDir(),
Name: workspaceID,
CreatedAt: now,
UpdatedAt: now,
}); err != nil {
t.Fatalf("InsertWorkspace() error = %v", err)
}
seedCoordinatorBackstopRun(t, db, now, "active-consumer", 0, workspaceID)
seedCoordinatorBackstopRun(t, db, now, "active-consumer", 1, workspaceID)
if _, err := db.ClaimNextRun(ctx, taskpkg.ClaimCriteria{
Scope: taskpkg.ScopeWorkspace,
WorkspaceID: workspaceID,
RunKind: taskpkg.RunKindCoordinator,
ClaimerSessionID: loopCoordinatorSessionID,
LeaseDuration: taskpkg.DefaultRunLeaseDuration,
Now: now,
}); err != nil {
t.Fatalf("ClaimNextRun(active coordinator) error = %v", err)
}
manager, err := taskpkg.NewManager(
taskpkg.WithStore(db),
taskpkg.WithManagerNow(func() time.Time { return now }),
)
if err != nil {
t.Fatalf("task.NewManager() error = %v", err)
}
actor, err := taskpkg.DeriveDaemonActorContext("scheduler", "daemon.scheduler")
if err != nil {
t.Fatalf("DeriveDaemonActorContext() error = %v", err)
}

started, err := (schedulerTaskSource{manager: manager, store: db}).RunLoopCoordinatorBackstop(ctx, now, actor)
if err != nil {
t.Fatalf("RunLoopCoordinatorBackstop() error = %v", err)
}
if started != 0 {
t.Fatalf("started = %d, want 0 while the coordinator consumer is busy", started)
}
queued, err := db.ListTaskRunsByStatus(ctx, []taskpkg.RunStatus{taskpkg.TaskRunStatusQueued})
if err != nil {
t.Fatalf("ListTaskRunsByStatus(queued) error = %v", err)
}
if got, want := len(queued), 1; got != want {
t.Fatalf("queued coordinator runs = %d, want %d deferred run", got, want)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Wrap the new test body in t.Run("Should …").

TestSchedulerTaskSourceLoopCoordinatorBackstopShouldDeferWhileConsumerLeaseIsActive calls t.Parallel() and runs its assertions directly, without a t.Run("Should …") subtest. Wrap the body in a subtest to follow the established test convention.

As per path instructions, "MUST use t.Run("Should...") pattern for ALL test cases."

♻️ Proposed fix to add the subtest wrapper
 func TestSchedulerTaskSourceLoopCoordinatorBackstopShouldDeferWhileConsumerLeaseIsActive(t *testing.T) {
 	t.Parallel()
-
-	ctx := testutil.Context(t)
-	now := time.Date(2026, 8, 3, 22, 45, 0, 0, time.UTC)
-	workspaceID := "ws-backstop-active-consumer"
-	db := openDaemonTestGlobalDB(t)
-	...
-	if got, want := len(queued), 1; got != want {
-		t.Fatalf("queued coordinator runs = %d, want %d deferred run", got, want)
-	}
+
+	t.Run("Should defer starting a coordinator run while the consumer session holds an active lease", func(t *testing.T) {
+		ctx := testutil.Context(t)
+		now := time.Date(2026, 8, 3, 22, 45, 0, 0, time.UTC)
+		workspaceID := "ws-backstop-active-consumer"
+		db := openDaemonTestGlobalDB(t)
+		...
+		if got, want := len(queued), 1; got != want {
+			t.Fatalf("queued coordinator runs = %d, want %d deferred run", got, want)
+		}
+	})
 }
📝 Committable suggestion

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

Suggested change
func TestSchedulerTaskSourceLoopCoordinatorBackstopShouldDeferWhileConsumerLeaseIsActive(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t)
now := time.Date(2026, 8, 3, 22, 45, 0, 0, time.UTC)
workspaceID := "ws-backstop-active-consumer"
db := openDaemonTestGlobalDB(t)
if err := db.InsertWorkspace(ctx, workspacepkg.Workspace{
ID: workspaceID,
RootDir: t.TempDir(),
Name: workspaceID,
CreatedAt: now,
UpdatedAt: now,
}); err != nil {
t.Fatalf("InsertWorkspace() error = %v", err)
}
seedCoordinatorBackstopRun(t, db, now, "active-consumer", 0, workspaceID)
seedCoordinatorBackstopRun(t, db, now, "active-consumer", 1, workspaceID)
if _, err := db.ClaimNextRun(ctx, taskpkg.ClaimCriteria{
Scope: taskpkg.ScopeWorkspace,
WorkspaceID: workspaceID,
RunKind: taskpkg.RunKindCoordinator,
ClaimerSessionID: loopCoordinatorSessionID,
LeaseDuration: taskpkg.DefaultRunLeaseDuration,
Now: now,
}); err != nil {
t.Fatalf("ClaimNextRun(active coordinator) error = %v", err)
}
manager, err := taskpkg.NewManager(
taskpkg.WithStore(db),
taskpkg.WithManagerNow(func() time.Time { return now }),
)
if err != nil {
t.Fatalf("task.NewManager() error = %v", err)
}
actor, err := taskpkg.DeriveDaemonActorContext("scheduler", "daemon.scheduler")
if err != nil {
t.Fatalf("DeriveDaemonActorContext() error = %v", err)
}
started, err := (schedulerTaskSource{manager: manager, store: db}).RunLoopCoordinatorBackstop(ctx, now, actor)
if err != nil {
t.Fatalf("RunLoopCoordinatorBackstop() error = %v", err)
}
if started != 0 {
t.Fatalf("started = %d, want 0 while the coordinator consumer is busy", started)
}
queued, err := db.ListTaskRunsByStatus(ctx, []taskpkg.RunStatus{taskpkg.TaskRunStatusQueued})
if err != nil {
t.Fatalf("ListTaskRunsByStatus(queued) error = %v", err)
}
if got, want := len(queued), 1; got != want {
t.Fatalf("queued coordinator runs = %d, want %d deferred run", got, want)
}
}
func TestSchedulerTaskSourceLoopCoordinatorBackstopShouldDeferWhileConsumerLeaseIsActive(t *testing.T) {
t.Parallel()
t.Run("Should defer starting a coordinator run while the consumer session holds an active lease", func(t *testing.T) {
ctx := testutil.Context(t)
now := time.Date(2026, 8, 3, 22, 45, 0, 0, time.UTC)
workspaceID := "ws-backstop-active-consumer"
db := openDaemonTestGlobalDB(t)
if err := db.InsertWorkspace(ctx, workspacepkg.Workspace{
ID: workspaceID,
RootDir: t.TempDir(),
Name: workspaceID,
CreatedAt: now,
UpdatedAt: now,
}); err != nil {
t.Fatalf("InsertWorkspace() error = %v", err)
}
seedCoordinatorBackstopRun(t, db, now, "active-consumer", 0, workspaceID)
seedCoordinatorBackstopRun(t, db, now, "active-consumer", 1, workspaceID)
if _, err := db.ClaimNextRun(ctx, taskpkg.ClaimCriteria{
Scope: taskpkg.ScopeWorkspace,
WorkspaceID: workspaceID,
RunKind: taskpkg.RunKindCoordinator,
ClaimerSessionID: loopCoordinatorSessionID,
LeaseDuration: taskpkg.DefaultRunLeaseDuration,
Now: now,
}); err != nil {
t.Fatalf("ClaimNextRun(active coordinator) error = %v", err)
}
manager, err := taskpkg.NewManager(
taskpkg.WithStore(db),
taskpkg.WithManagerNow(func() time.Time { return now }),
)
if err != nil {
t.Fatalf("task.NewManager() error = %v", err)
}
actor, err := taskpkg.DeriveDaemonActorContext("scheduler", "daemon.scheduler")
if err != nil {
t.Fatalf("DeriveDaemonActorContext() error = %v", err)
}
started, err := (schedulerTaskSource{manager: manager, store: db}).RunLoopCoordinatorBackstop(ctx, now, actor)
if err != nil {
t.Fatalf("RunLoopCoordinatorBackstop() error = %v", err)
}
if started != 0 {
t.Fatalf("started = %d, want 0 while the coordinator consumer is busy", started)
}
queued, err := db.ListTaskRunsByStatus(ctx, []taskpkg.RunStatus{taskpkg.TaskRunStatusQueued})
if err != nil {
t.Fatalf("ListTaskRunsByStatus(queued) error = %v", err)
}
if got, want := len(queued), 1; got != want {
t.Fatalf("queued coordinator runs = %d, want %d deferred run", got, want)
}
})
}
🤖 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_runtime_test.go` around lines 366 - 421, Wrap the
existing test body in
TestSchedulerTaskSourceLoopCoordinatorBackstopShouldDeferWhileConsumerLeaseIsActive
inside a t.Run subtest named with the established “Should …” pattern, while
preserving t.Parallel(), setup, and assertions.

Source: Path instructions

Comment on lines +151 to +160
runtimeRef := generationOutputRuntimeRef(output)
if strings.TrimSpace(runtimeRef) == "" {
return nil, fmt.Errorf(
"%w: persisted route-causing gate %q finished without an output reference",
ErrValidation,
record.GateID,
)
}
var verdict gate.Verdict
if err := json.Unmarshal([]byte(output.OutputRef), &verdict); err != nil {
if err := json.Unmarshal([]byte(runtimeRef), &verdict); err != nil {

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Trace hydration of GenerationOutput runtime refs around loadPersistedRouteCauses.
set -euo pipefail

echo "== callers of loadPersistedRouteCauses =="
rg -n -C6 'loadPersistedRouteCauses' internal/loop

echo "== definitions of the runtime-ref helpers =="
rg -n -C8 'func setGenerationOutputRef|func generationOutputRuntimeRef|func generationOutputRuntimeView' internal/loop

echo "== other callers of generationOutputRuntimeView for comparison =="
rg -n -B3 -A10 'generationOutputRuntimeView\(' internal/loop

Repository: compozy/compozy

Length of output: 14239


🏁 Script executed:

#!/bin/bash
# Inspect the read path that constructs `normalized` before it reaches loadPersistedRouteCauses.
set -euo pipefail

echo "== coordinator_generation_succession.go relevant section =="
wc -l internal/loop/coordinator_generation_succession.go
sed -n '1,230p' internal/loop/coordinator_generation_succession.go | cat -n

echo "== persisted route causes implementation =="
sed -n '80,185p' internal/loop/coordinator_succession.go | cat -n

echo "== GenerationOutput runtime helper implementation =="
sed -n '1,120p' internal/loop/generation_output_runtime.go | cat -n

echo "== tests around route cause inputs =="
sed -n '1920,2010p' internal/loop/coordinator_test.go | cat -n

Repository: compozy/compozy

Length of output: 20300


🏁 Script executed:

#!/bin/bash
# Locate the callers of buildFailedGenerationPlan and inspect how normalized is populated.
set -euo pipefail

echo "== buildFailedGenerationPlan callers =="
rg -n -C8 'buildFailedGenerationPlan\(' internal/loop

echo "== generation output persistence definitions/usages =="
rg -n -C4 'OutputRefLooksContentAddressed|OutputRefForPayload|SetGenerationOutput|generationOutputRuntimeScope\{|runtimePayload' internal/loop

Repository: compozy/compozy

Length of output: 28704


Hydrate persisted route-causing outputs before decoding runtime refs.

loadPersistedRouteCauses reads generationOutputRuntimeRef(output) directly in internal/loop/coordinator_succession.go. When that reference is content-addressed, the function decodes the sha256:... key instead of the verdict JSON. Hydrate outputs with generationOutputRuntimeView in this failure/reattempt path, or hydrate it before building the persisted route-causes input.

🤖 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_succession.go` around lines 151 - 160, Update
loadPersistedRouteCauses to hydrate persisted outputs with
generationOutputRuntimeView before calling generationOutputRuntimeRef, ensuring
content-addressed references resolve to the verdict JSON before json.Unmarshal.
Apply this in the failure/reattempt path while preserving the existing
validation and error handling.

Comment on lines +140 to +173
<details
className="mt-2"
open={(shape === "emit" ? row.emit?.payload : row.with) !== undefined || undefined}
>
<summary className="cursor-pointer font-mono text-mono-id text-subtle">
{shape === "emit" ? "payload" : "with"} · optional
</summary>
<div className="mt-1">
<LoopEditorJsonField
value={shape === "emit" ? row.emit?.payload : row.with}
disabled={disabled}
placeholder={shape === "emit" ? "event payload" : "tool arguments"}
ariaLabel={`${rowName} ${shape === "emit" ? "payload" : "arguments"}`}
testId={`${testId}-${shape === "emit" ? "payload" : "with"}-${index}`}
className="min-h-14 resize-y text-form-input leading-relaxed"
onCommit={parsed =>
replace(
index,
shape === "emit"
? {
emit: {
kind: str(row.emit?.kind),
...(parsed === undefined ? {} : { payload: parsed }),
},
}
: {
tool: str(row.tool),
...(parsed === undefined ? {} : { with: parsed }),
}
)
}
/>
</div>
</details>

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix stale JSON text after switching effect shape.

withShape (lines 48-51) correctly drops payload/with when the user switches between emit and tool. But the LoopEditorJsonField rendered here keeps its own text state, seeded once from value on mount. Since no key ties this instance to shape, switching shape does not remount it: the textarea keeps showing the old JSON even though the underlying effect no longer carries that payload or argument.

A user can therefore believe a payload is still attached after switching shapes, when the committed row actually has none. Force a remount on shape change so the field re-derives its text from the new value.

🐛 Proposed fix: remount the JSON field on shape switch
               <div className="mt-1">
                 <LoopEditorJsonField
+                  key={shape}
                   value={shape === "emit" ? row.emit?.payload : row.with}
                   disabled={disabled}
                   placeholder={shape === "emit" ? "event payload" : "tool arguments"}
                   ariaLabel={`${rowName} ${shape === "emit" ? "payload" : "arguments"}`}
                   testId={`${testId}-${shape === "emit" ? "payload" : "with"}-${index}`}
                   className="min-h-14 resize-y text-form-input leading-relaxed"
                   onCommit={parsed =>
📝 Committable suggestion

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

Suggested change
<details
className="mt-2"
open={(shape === "emit" ? row.emit?.payload : row.with) !== undefined || undefined}
>
<summary className="cursor-pointer font-mono text-mono-id text-subtle">
{shape === "emit" ? "payload" : "with"} · optional
</summary>
<div className="mt-1">
<LoopEditorJsonField
value={shape === "emit" ? row.emit?.payload : row.with}
disabled={disabled}
placeholder={shape === "emit" ? "event payload" : "tool arguments"}
ariaLabel={`${rowName} ${shape === "emit" ? "payload" : "arguments"}`}
testId={`${testId}-${shape === "emit" ? "payload" : "with"}-${index}`}
className="min-h-14 resize-y text-form-input leading-relaxed"
onCommit={parsed =>
replace(
index,
shape === "emit"
? {
emit: {
kind: str(row.emit?.kind),
...(parsed === undefined ? {} : { payload: parsed }),
},
}
: {
tool: str(row.tool),
...(parsed === undefined ? {} : { with: parsed }),
}
)
}
/>
</div>
</details>
<details
className="mt-2"
open={(shape === "emit" ? row.emit?.payload : row.with) !== undefined || undefined}
>
<summary className="cursor-pointer font-mono text-mono-id text-subtle">
{shape === "emit" ? "payload" : "with"} · optional
</summary>
<div className="mt-1">
<LoopEditorJsonField
key={shape}
value={shape === "emit" ? row.emit?.payload : row.with}
disabled={disabled}
placeholder={shape === "emit" ? "event payload" : "tool arguments"}
ariaLabel={`${rowName} ${shape === "emit" ? "payload" : "arguments"}`}
testId={`${testId}-${shape === "emit" ? "payload" : "with"}-${index}`}
className="min-h-14 resize-y text-form-input leading-relaxed"
onCommit={parsed =>
replace(
index,
shape === "emit"
? {
emit: {
kind: str(row.emit?.kind),
...(parsed === undefined ? {} : { payload: parsed }),
},
}
: {
tool: str(row.tool),
...(parsed === undefined ? {} : { with: parsed }),
}
)
}
/>
</div>
</details>
🤖 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 `@web/src/systems/loops/components/editor/loop-editor-effects.tsx` around lines
140 - 173, Add a React key to the LoopEditorJsonField instance based on the
current effect shape, such as shape, so switching between emit and tool remounts
the field and reinitializes its text from the updated value. Keep the existing
value, commit behavior, and other props unchanged.

Comment on lines +406 to +412
case "effect_results": {
const result = parseEffectResult(frame, payload);
if (result) {
next.effectResults = [...state.effectResults, result].slice(-MAX_EFFECT_RESULTS);
}
break;
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Replayed frames duplicate effect results and can revive dropped retry schedules.

retainFrame at Line 332 drops a frame whose seq is not newer than the last retained frame, so a reconnect replay does not duplicate story history. The switch below runs for every frame, including those replayed frames. Two consequences follow:

  • effect_results appends the same delivery_id again, so the Inspect surface lists one delivery several times.
  • A replayed node_retry_scheduled that arrives after node_resumed reinstates a schedule the reducer already dropped, so the run page counts down to an attempt that will not run.

Guard the state updates with the same sequence rule, or deduplicate effectResults by deliveryId.

🐛 Proposed fix for the duplicate delivery case
     case "effect_results": {
       const result = parseEffectResult(frame, payload);
-      if (result) {
+      if (result && !state.effectResults.some(item => item.deliveryId === result.deliveryId)) {
         next.effectResults = [...state.effectResults, result].slice(-MAX_EFFECT_RESULTS);
       }
       break;
     }
🤖 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 `@web/src/systems/loops/lib/loop-events.ts` around lines 406 - 412, Guard
frame-driven state updates in the switch, including effect_results and
node_retry_scheduled, with the same newer-sequence rule used by retainFrame so
replayed frames cannot mutate state. Ensure effectResults does not append an
existing deliveryId, and preserve the existing behavior for genuinely newer
frames.

Comment on lines +159 to +170
function cancelingLine(node: LoopNodeLifecycle): LoopNodeNowLine {
const provenance = node.cancelProvenance;
const actor = provenance ? actorLabel(provenance.actor_kind, provenance.actor_id) : "";
return {
nodeId: node.nodeId,
state: node.cancelState,
headline: `${node.label} is winding down`,
body: `Cancellation ${node.cancelState}${actor === "" ? "" : ` — requested by ${actor}`}. Its in-flight work drains before the lane closes.`,
chip: null,
provenance: null,
micro: `node_controls.cancel_state ${node.cancelState} · ${node.nodeId} · gen ${node.generation}`,
};

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

cancelingLine publishes the raw cancel_state instead of the normalized lifecycle token.

lifecycleState in web/src/systems/loops/lib/loop-node-lifecycle.ts line 145 maps every non-empty, non-canceled cancel_state to the single token "canceling". buildNodeNowLines reaches this function only through case "canceling". Line 164 then discards that normalization and sets state to the raw wire value, for example "draining" or "requested". The state field drives the dot treatment and the trailing label, so a consumer that switches on the documented lifecycle tokens will not match this row and will fall through to a default treatment.

Line 166 has the same root cause: it renders the raw wire token inside user prose (Cancellation draining). Map the token to plain-language copy.

🐛 Proposed fix
 function cancelingLine(node: LoopNodeLifecycle): LoopNodeNowLine {
   const provenance = node.cancelProvenance;
   const actor = provenance ? actorLabel(provenance.actor_kind, provenance.actor_id) : "";
   return {
     nodeId: node.nodeId,
-    state: node.cancelState,
+    state: "canceling",
     headline: `${node.label} is winding down`,
-    body: `Cancellation ${node.cancelState}${actor === "" ? "" : ` — requested by ${actor}`}. Its in-flight work drains before the lane closes.`,
+    body: `Cancellation is in progress${actor === "" ? "" : ` — requested by ${actor}`}. Its in-flight work drains before the lane closes.`,
     chip: null,
     provenance: null,
     micro: `node_controls.cancel_state ${node.cancelState} · ${node.nodeId} · gen ${node.generation}`,
   };
 }

The micro line keeps the raw token, so the identity trail still shows the daemon value.

📝 Committable suggestion

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

Suggested change
function cancelingLine(node: LoopNodeLifecycle): LoopNodeNowLine {
const provenance = node.cancelProvenance;
const actor = provenance ? actorLabel(provenance.actor_kind, provenance.actor_id) : "";
return {
nodeId: node.nodeId,
state: node.cancelState,
headline: `${node.label} is winding down`,
body: `Cancellation ${node.cancelState}${actor === "" ? "" : ` — requested by ${actor}`}. Its in-flight work drains before the lane closes.`,
chip: null,
provenance: null,
micro: `node_controls.cancel_state ${node.cancelState} · ${node.nodeId} · gen ${node.generation}`,
};
function cancelingLine(node: LoopNodeLifecycle): LoopNodeNowLine {
const provenance = node.cancelProvenance;
const actor = provenance ? actorLabel(provenance.actor_kind, provenance.actor_id) : "";
return {
nodeId: node.nodeId,
state: "canceling",
headline: `${node.label} is winding down`,
body: `Cancellation is in progress${actor === "" ? "" : ` — requested by ${actor}`}. Its in-flight work drains before the lane closes.`,
chip: null,
provenance: null,
micro: `node_controls.cancel_state ${node.cancelState} · ${node.nodeId} · gen ${node.generation}`,
};
}
🤖 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 `@web/src/systems/loops/lib/loop-node-now-view.ts` around lines 159 - 170,
Update cancelingLine to use the normalized lifecycle token "canceling" for the
returned state instead of node.cancelState, while preserving node.cancelState in
the micro identity trail. Replace the raw cancel state in the user-facing body
text with plain-language cancellation copy that does not expose wire values such
as "draining" or "requested".

Comment on lines +175 to +191
export function quarantineChainRows(entry: LoopQuarantineEntry): LoopQuarantineChainRow[] {
const rows: LoopQuarantineChainRow[] = [];
entry.episodes.forEach((episode, episodeIndex) => {
episode.attempts.forEach((attempt, attemptIndex) => {
rows.push({
...attempt,
key: `${episodeIndex}-${attemptIndex}-${attempt.attempt}`,
episodeIndex,
generation: episode.generation,
// A requeue precedes every episode after the first; index n-1 opened episode n.
openedBy:
attemptIndex === 0 && episodeIndex > 0 ? entry.requeues[episodeIndex - 1] : undefined,
});
});
});
return rows;
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the Go quarantine entry payload shape: episode/requeue ordering, generation fields, truncation.
rg -n -C 6 'quarantine_entry|QuarantineEntry' --glob '*.go' | head -200

Repository: compozy/compozy

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
fd -i 'loop-quarantine-entry|quarantine|loop.*entry' . | head -100

echo
echo "== search quarantine-related symbols =="
rg -n "quarantine|Quarantine|requeue|Requeue|generation|Episode" --glob '*.go' --glob '*.ts' --glob '*.tsx' . | head -300

Repository: compozy/compozy

Length of output: 36308


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file outline/lines =="
wc -l web/src/systems/loops/lib/loop-quarantine-entry.ts
sed -n '1,240p' web/src/systems/loops/lib/loop-quarantine-entry.ts

echo
echo "== quarantine Go files outline/lines =="
wc -l internal/loop/quarantine.go internal/loop/coordinator_quarantine.go
sed -n '1,260p' internal/loop/quarantine.go
sed -n '1,280p' internal/loop/coordinator_quarantine.go

echo
echo "== quarantine-related openapi/types references =="
rg -n "quarantine|request|requeues|episodes|generation" sdk/typescript/src/generated/contracts.ts sdk/go/contracts/types*_gen.go internal/loop --glob '*.go' | head -300

Repository: compozy/compozy

Length of output: 50371


Match openedBy to the retained episode by generation.

When entry.truncated is true, encodeQuarantineEntry can keep more recent episodes while dropping older requeues, so entry.requeues[episodeIndex - 1] can name the wrong requeue. Use each episode’s generation to find the matching requeue, and leave openedBy undefined when no retained requeue matches.

🤖 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 `@web/src/systems/loops/lib/loop-quarantine-entry.ts` around lines 175 - 191,
The openedBy assignment in quarantineChainRows must match retained episodes by
generation rather than relying on the truncated array index. For the first
attempt of each non-initial episode, find the requeue whose generation opens
episode.generation and assign it; leave openedBy undefined when no matching
retained requeue exists.

pedronauck and others added 8 commits August 3, 2026 20:58
Checkpoint via cy-loop-tasks (iteration 11, phase B mode=tasks).

Full gate fingerprint 0aa5cdf; VC-R1..R5 and targeted lifecycle QA passed with clean teardown.

Co-Authored-By: Codex <[email protected]>
Checkpoint via cy-loop-tasks (iteration 12, phase B mode=tasks).

Co-Authored-By: Codex <[email protected]>
Checkpoint via cy-loop-tasks (iteration 13, phase B mode=tasks).

Co-Authored-By: Codex <[email protected]>
Checkpoint via cy-loop-tasks (iteration 14, phase B mode=tasks).

Fumadocs generation, site lint/typecheck/build, 312 tests, navigation checks, and codegen-check passed.

Co-Authored-By: Codex <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
web/src/systems/loops/lib/loop-reliability.ts (1)

1-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the required @/* import alias.

Replace the relative imports with @/systems/loops/... imports.

Proposed change
-import type { LoopGraph } from "./loop-graph";
-import type { LoopDetail } from "../types";
+import type { LoopGraph } from "`@/systems/loops/lib/loop-graph`";
+import type { LoopDetail } from "`@/systems/loops/types`";

As per coding guidelines, use the @/* path alias for ./src/*.

🤖 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 `@web/src/systems/loops/lib/loop-reliability.ts` around lines 1 - 2, Update the
type imports for LoopGraph and LoopDetail in loop-reliability.ts to use the
configured `@/systems/loops/`... path alias instead of relative paths, preserving
the existing imported symbols and type-only behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/systems/loops/lib/loop-reliability.ts`:
- Around line 79-82: Update the expiry message construction near `expires` and
`expiry` so it only mentions a fallback path when the graph exposes a configured
expiry route; otherwise use neutral wording that describes the wait timeout
without claiming a fallback. Preserve the existing duration formatting and
behavior when no expiry is present.

In `@web/src/systems/loops/mocks/lint-definition.ts`:
- Around line 29-49: Update durationMs to parse the full Go-compatible duration
syntax used by time.ParseDuration, including composite values such as 1m30s,
while preserving null for invalid input. Ensure lintNodeLifecycle can validate
timeout_exceeds_deadline for composite timeout and deadline fields instead of
treating them as duration_invalid.
- Around line 92-93: Update emitInvalid in the lint-definition validation so any
present effect.with key is rejected for emits, regardless of its value or type,
including null. Preserve the existing invalid-kind and hasEmit/hasTool
validation behavior.

---

Nitpick comments:
In `@web/src/systems/loops/lib/loop-reliability.ts`:
- Around line 1-2: Update the type imports for LoopGraph and LoopDetail in
loop-reliability.ts to use the configured `@/systems/loops/`... path alias instead
of relative paths, preserving the existing imported symbols and type-only
behavior.
🪄 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: 0ee489c4-d9a8-492a-90f0-6beef440de6c

📥 Commits

Reviewing files that changed from the base of the PR and between 4cdbea0 and b067b70.

⛔ Files ignored due to path filters (92)
  • docs/_memory/glossary.md is excluded by !**/*.md
  • docs/qa/automation-backlog/all-run-status-seeds.md is excluded by !**/*.md
  • docs/qa/bugs/BUG-20260803-agent-workspace-id-disagrees.md is excluded by !**/*.md
  • docs/qa/bugs/BUG-20260803-cross-origin-coordinator-duplicate.md is excluded by !**/*.md
  • docs/qa/bugs/BUG-20260803-disabled-extension-blocks-wait-resume.md is excluded by !**/*.md
  • docs/qa/bugs/BUG-20260803-error-route-fallback-blocked.md is excluded by !**/*.md
  • docs/qa/bugs/BUG-20260803-loop-boot-active-coordinator-lease.md is excluded by !**/*.md
  • docs/qa/bugs/BUG-20260803-wait-event-rejected-too-late.md is excluded by !**/*.md
  • docs/qa/charters/CH-004.md is excluded by !**/*.md
  • docs/qa/charters/CH-011.md is excluded by !**/*.md
  • docs/qa/charters/CH-022.md is excluded by !**/*.md
  • docs/qa/charters/CH-023.md is excluded by !**/*.md
  • docs/qa/charters/CH-024.md is excluded by !**/*.md
  • docs/qa/charters/CH-026.md is excluded by !**/*.md
  • docs/qa/charters/CH-agent-loop-lifecycle-parity.md is excluded by !**/*.md
  • docs/qa/charters/CH-approver-loop-wait.md is excluded by !**/*.md
  • docs/qa/charters/CH-author-loop-failure-contract.md is excluded by !**/*.md
  • docs/qa/charters/CH-operator-lifecycle-ui.md is excluded by !**/*.md
  • docs/qa/charters/CH-operator-loop-recovery.md is excluded by !**/*.md
  • docs/qa/journeys/J-03-observe-and-approve.md is excluded by !**/*.md
  • docs/qa/journeys/J-07-agent-operated-run.md is excluded by !**/*.md
  • docs/qa/journeys/J-16-watch-events-wake.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-resume-dead-loop-node.md is excluded by !**/*.md
  • docs/qa/personas.md is excluded by !**/*.md
  • docs/qa/reports/2026-08-03-loop-node-lifecycle-plan.md is excluded by !**/*.md
  • docs/qa/reports/2026-08-03-loop-node-lifecycle-task08.md is excluded by !**/*.md
  • docs/qa/reports/2026-08-03-loop-node-lifecycle.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-016.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-026.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-040.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-041.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-042.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-044.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-047.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-048.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-049.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-050.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-agent-operates-lifecycle-via-native-tools.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-approval-link-journey.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-cancel-vs-kill.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-catalog-runform-walk.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-crash-death-resume.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-days-long-node-no-clock.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-duplicate-event-suppressed.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-durable-wait-restart.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-editor-authoring-walk.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-error-route-fallback.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-live-pause-repair-resume.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-live-run-survives-extension-disable.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-loop-run-deep-link.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-on-error-notification-with-context.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-operator-lifecycle-ui.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-quarantine-diagnose-requeue.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-run-detail-story-redesign.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/LP-wait-event-catalog-validation.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-waiting-inventory-escalation.md is excluded by !**/*.md
  • docs/qa/scenarios/TA-070.md is excluded by !**/*.md
  • docs/qa/scenarios/TA-076.md is excluded by !**/*.md
  • docs/qa/scenarios/TA-084.md is excluded by !**/*.md
  • docs/qa/scenarios/TA-action-run-liveness.md is excluded by !**/*.md
  • docs/qa/scenarios/TA-loop-failure-breaker.md is excluded by !**/*.md
  • openapi/compozy.json is excluded by !**/*.json
  • packages/site/content/docs/configuration/config-toml.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/loops/catalog.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/loops/configure.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/loops/dsl-reference.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/loops/extensions.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/loops/failure-handling.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/loops/guardrails.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/loops/index.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/loops/meta.json is excluded by !**/*.json
  • packages/site/content/docs/loops/reference-grammar.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/loops/running.mdx is excluded by !**/*.mdx
  • packages/ui/src/components/stories/confirm-dialog.stories.tsx is excluded by !**/*.stories.tsx
  • skills/compozy/references/loops.md is excluded by !**/*.md
  • web/e2e/__tests__/loops.spec.ts is excluded by !web/e2e/**
  • web/e2e/__tests__/os-shell.spec.ts is excluded by !web/e2e/**
  • web/src/generated/compozy-openapi.d.ts is excluded by !**/generated/**, !**/generated/**, !**/*.d.ts
  • web/src/systems/loops/components/stories/loop-catalog.stories.tsx is excluded by !**/*.stories.tsx
  • web/src/systems/loops/components/stories/loop-detail.stories.tsx is excluded by !**/*.stories.tsx
  • web/src/systems/loops/components/stories/loop-editor.stories.tsx is excluded by !**/*.stories.tsx
  • web/src/systems/loops/components/stories/loop-node-controls.stories.tsx is excluded by !**/*.stories.tsx
  • web/src/systems/loops/components/stories/loop-node-inventory.stories.tsx is excluded by !**/*.stories.tsx
  • web/src/systems/loops/components/stories/loop-quarantine-sheet.stories.tsx is excluded by !**/*.stories.tsx
  • web/src/systems/loops/components/stories/loop-run-form.stories.tsx is excluded by !**/*.stories.tsx
  • web/src/systems/loops/components/stories/loop-run-page.stories.tsx is excluded by !**/*.stories.tsx
📒 Files selected for processing (223)
  • internal/api/contract/contract_test.go
  • internal/api/contract/loop_lifecycle.go
  • internal/api/contract/loops.go
  • internal/api/core/agent_create.go
  • internal/api/core/agent_definition_resolution.go
  • internal/api/core/handlers_agents.go
  • internal/api/core/handlers_test.go
  • internal/api/core/loops_test.go
  • internal/api/spec/loop_schemas.go
  • internal/api/spec/schema_customizers.go
  • internal/daemon/daemon_mock_agents_integration_test.go
  • internal/daemon/daemon_review_and_fix_e2e_integration_test.go
  • internal/daemon/loop_action_policy.go
  • internal/daemon/loop_api_runs_test.go
  • internal/daemon/loop_managed_binding.go
  • internal/daemon/native_create_tools.go
  • internal/daemon/native_create_tools_test.go
  • internal/daemon/scheduler_loop_coordinator.go
  • internal/daemon/scheduler_runtime_test.go
  • internal/loop/action_internal_test.go
  • internal/loop/action_runagent.go
  • internal/loop/action_runtime_helpers.go
  • internal/loop/action_test.go
  • internal/loop/action_types.go
  • internal/loop/control_file_import.go
  • internal/loop/control_gate.go
  • internal/loop/control_input.go
  • internal/loop/control_namespace.go
  • internal/loop/control_namespace_history.go
  • internal/loop/control_namespace_history_test.go
  • internal/loop/control_plan.go
  • internal/loop/control_plan_branch.go
  • internal/loop/control_readiness.go
  • internal/loop/control_tasks.go
  • internal/loop/coordinator.go
  • internal/loop/coordinator_action.go
  • internal/loop/coordinator_control_test.go
  • internal/loop/coordinator_error_routes.go
  • internal/loop/coordinator_generation.go
  • internal/loop/coordinator_goal_control.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_outputs.go
  • internal/loop/coordinator_succession.go
  • internal/loop/coordinator_terminal_helpers.go
  • internal/loop/coordinator_test.go
  • internal/loop/coordinator_wait.go
  • internal/loop/coordinator_watch.go
  • internal/loop/coordinator_watch_events.go
  • internal/loop/fanout_materialization.go
  • internal/loop/generation_output_runtime.go
  • internal/loop/generation_snapshot.go
  • internal/loop/goal/types_binding.go
  • internal/loop/linter_test.go
  • internal/loop/linter_wait.go
  • internal/loop/node_attempt.go
  • internal/loop/output_ref.go
  • internal/loop/service_control.go
  • internal/loop/service_node_requeue.go
  • internal/loop/service_pinned_definition.go
  • internal/loop/service_test.go
  • internal/loop/watch_events_registry_phase_a.go
  • internal/store/globaldb/global_db_goal_binding_activation_cleanup.go
  • internal/store/globaldb/global_db_goal_binding_activation_owner.go
  • internal/store/globaldb/global_db_goal_binding_cleanup.go
  • internal/store/globaldb/global_db_goal_binding_integration_test.go
  • internal/store/globaldb/global_db_goal_binding_transitions.go
  • internal/store/globaldb/global_db_goal_checkpoint_integration_test.go
  • internal/store/globaldb/global_db_goal_session_cleanup.go
  • internal/store/globaldb/global_db_goal_turn_runtime_integration_test.go
  • internal/store/globaldb/global_db_loop_outputs.go
  • internal/store/globaldb/global_db_task_claim_test.go
  • internal/store/globaldb/global_db_task_coordinator.go
  • internal/store/globaldb/global_db_task_coordinator_progress.go
  • internal/store/globaldb/global_db_task_coordinator_runtime.go
  • internal/store/globaldb/queries/loop_runtime.sql
  • internal/store/globaldb/sqlcgen/loop_runtime.sql.go
  • packages/ui/src/components/__tests__/confirm-dialog.test.tsx
  • packages/ui/src/components/custom/confirm-dialog.tsx
  • web/src/systems/loops/adapters/loop-nodes-api.ts
  • web/src/systems/loops/adapters/loops-api-errors.ts
  • web/src/systems/loops/adapters/loops-api.ts
  • web/src/systems/loops/adapters/loops-runs-api.ts
  • web/src/systems/loops/components/__tests__/loop-catalog.test.tsx
  • web/src/systems/loops/components/__tests__/loop-detail.test.tsx
  • web/src/systems/loops/components/__tests__/loop-editor-field.test.tsx
  • web/src/systems/loops/components/__tests__/loop-editor.test.tsx
  • web/src/systems/loops/components/__tests__/loop-linter-dock.test.tsx
  • web/src/systems/loops/components/__tests__/loop-run-form.test.tsx
  • web/src/systems/loops/components/__tests__/loop-run-overrides.test.tsx
  • web/src/systems/loops/components/__tests__/loop-run-page.test.tsx
  • web/src/systems/loops/components/__tests__/loop-start-bindings-panel.test.tsx
  • web/src/systems/loops/components/catalog/loop-catalog-card.tsx
  • web/src/systems/loops/components/catalog/loop-catalog-filters.tsx
  • web/src/systems/loops/components/catalog/loop-catalog-lede.tsx
  • web/src/systems/loops/components/catalog/loop-catalog-row.tsx
  • web/src/systems/loops/components/catalog/loop-catalog.tsx
  • web/src/systems/loops/components/detail/loop-contract-panel.tsx
  • web/src/systems/loops/components/detail/loop-declared-inputs.tsx
  • web/src/systems/loops/components/detail/loop-detail.tsx
  • web/src/systems/loops/components/detail/loop-limits-panel.tsx
  • web/src/systems/loops/components/detail/loop-recent-runs.tsx
  • web/src/systems/loops/components/detail/loop-reliability-panel.tsx
  • web/src/systems/loops/components/detail/loop-start-bindings-panel.tsx
  • web/src/systems/loops/components/detail/loop-stats-panel.tsx
  • web/src/systems/loops/components/detail/loop-versions-panel.tsx
  • web/src/systems/loops/components/editor/loop-editor-canvas.tsx
  • web/src/systems/loops/components/editor/loop-editor-contract-terminals.tsx
  • web/src/systems/loops/components/editor/loop-editor-contract.tsx
  • web/src/systems/loops/components/editor/loop-editor-effects.tsx
  • web/src/systems/loops/components/editor/loop-editor-field-effects.tsx
  • web/src/systems/loops/components/editor/loop-editor-field.tsx
  • web/src/systems/loops/components/editor/loop-editor-inspector.tsx
  • web/src/systems/loops/components/editor/loop-editor-json-field.tsx
  • web/src/systems/loops/components/editor/loop-editor-publish-rejected-strip.tsx
  • web/src/systems/loops/components/editor/loop-editor-sidebar.tsx
  • web/src/systems/loops/components/editor/loop-editor-source-strip.tsx
  • web/src/systems/loops/components/editor/loop-editor-toolbar.tsx
  • web/src/systems/loops/components/editor/loop-editor-wait-mode.tsx
  • web/src/systems/loops/components/editor/loop-editor.tsx
  • web/src/systems/loops/components/editor/loop-linter-dock.tsx
  • web/src/systems/loops/components/loop-page-lede.tsx
  • web/src/systems/loops/components/loop-rail-section.tsx
  • web/src/systems/loops/components/run-form/loop-run-active-notice.tsx
  • web/src/systems/loops/components/run-form/loop-run-after-start.tsx
  • web/src/systems/loops/components/run-form/loop-run-form.tsx
  • web/src/systems/loops/components/run-form/loop-run-overrides.tsx
  • web/src/systems/loops/components/run-form/loop-run-preview.tsx
  • web/src/systems/loops/components/run-form/loop-run-ways-to-start.tsx
  • web/src/systems/loops/components/run-page/loop-node-control-dialog.tsx
  • web/src/systems/loops/components/run-page/loop-node-control-menu.tsx
  • web/src/systems/loops/components/run-page/loop-node-row-actions.tsx
  • web/src/systems/loops/components/run-page/loop-quarantine-sheet.tsx
  • web/src/systems/loops/components/run-page/loop-run-control-dialog.tsx
  • web/src/systems/loops/components/run-page/loop-run-controls.tsx
  • web/src/systems/loops/components/run-page/loop-run-now-card.tsx
  • web/src/systems/loops/components/run-page/loop-run-outcome-card.tsx
  • web/src/systems/loops/components/run-page/loop-run-overflow-menu.tsx
  • web/src/systems/loops/components/run-page/loop-run-page-body.tsx
  • web/src/systems/loops/components/run-page/loop-run-parked-panels.tsx
  • web/src/systems/loops/components/run-page/loop-run-progress-panel.tsx
  • web/src/systems/loops/components/run-page/loop-run-story-timeline.tsx
  • web/src/systems/loops/components/run-page/loop-run-waits-rail.tsx
  • web/src/systems/loops/components/runs/loop-node-inventory-view.tsx
  • web/src/systems/loops/components/stories/loop-run-lifecycle-fixtures.ts
  • web/src/systems/loops/components/stories/loop-run-page-fixtures.ts
  • web/src/systems/loops/components/stories/loop-run-scenario-props.ts
  • web/src/systems/loops/components/stories/loop-run-scenario-types.ts
  • web/src/systems/loops/components/stories/loops-story-shell.tsx
  • web/src/systems/loops/hooks/__tests__/use-loop-actions.test.tsx
  • web/src/systems/loops/hooks/__tests__/use-loops.test.tsx
  • web/src/systems/loops/hooks/loop-editor-store-contracts.ts
  • web/src/systems/loops/hooks/loop-editor-store-draft.ts
  • web/src/systems/loops/hooks/loop-editor-store-publish.ts
  • web/src/systems/loops/hooks/use-loop-actions.ts
  • web/src/systems/loops/hooks/use-loop-editor-state.ts
  • web/src/systems/loops/hooks/use-loop-editor.ts
  • web/src/systems/loops/hooks/use-loop-fork.ts
  • web/src/systems/loops/hooks/use-loop-node-actions.ts
  • web/src/systems/loops/hooks/use-loop-node-inventory.ts
  • web/src/systems/loops/index.ts
  • web/src/systems/loops/lib/__tests__/loop-catalog.test.ts
  • web/src/systems/loops/lib/__tests__/loop-editor-draft.test.ts
  • web/src/systems/loops/lib/__tests__/loop-editor-lint.test.ts
  • web/src/systems/loops/lib/__tests__/loop-events.test.ts
  • web/src/systems/loops/lib/__tests__/loop-list-filters.test.ts
  • web/src/systems/loops/lib/__tests__/loop-node-schema.test.ts
  • web/src/systems/loops/lib/__tests__/loop-reliability.test.ts
  • web/src/systems/loops/lib/__tests__/loop-run-progress.test.ts
  • web/src/systems/loops/lib/__tests__/loop-run-story.test.ts
  • web/src/systems/loops/lib/__tests__/loop-runs-view.test.ts
  • web/src/systems/loops/lib/loop-catalog-presentation.ts
  • web/src/systems/loops/lib/loop-catalog.ts
  • web/src/systems/loops/lib/loop-contract-terminal-fields.ts
  • web/src/systems/loops/lib/loop-editor-definition.ts
  • web/src/systems/loops/lib/loop-editor-draft.ts
  • web/src/systems/loops/lib/loop-editor-lint.ts
  • web/src/systems/loops/lib/loop-events.ts
  • web/src/systems/loops/lib/loop-graph.ts
  • web/src/systems/loops/lib/loop-list-filters.ts
  • web/src/systems/loops/lib/loop-node-controls.ts
  • web/src/systems/loops/lib/loop-node-fields.ts
  • web/src/systems/loops/lib/loop-node-inventory.ts
  • web/src/systems/loops/lib/loop-node-lifecycle-fields.ts
  • web/src/systems/loops/lib/loop-node-lifecycle.ts
  • web/src/systems/loops/lib/loop-node-now-view.ts
  • web/src/systems/loops/lib/loop-node-schema-types.ts
  • web/src/systems/loops/lib/loop-node-schema.ts
  • web/src/systems/loops/lib/loop-node-verb-copy.ts
  • web/src/systems/loops/lib/loop-node-wait-fields.ts
  • web/src/systems/loops/lib/loop-overrides.ts
  • web/src/systems/loops/lib/loop-palette.ts
  • web/src/systems/loops/lib/loop-quarantine-entry.ts
  • web/src/systems/loops/lib/loop-reliability.ts
  • web/src/systems/loops/lib/loop-run-page-view.ts
  • web/src/systems/loops/lib/loop-run-progress.ts
  • web/src/systems/loops/lib/loop-run-story-lifecycle-rows.ts
  • web/src/systems/loops/lib/loop-run-story-types.ts
  • web/src/systems/loops/lib/loop-run-story.ts
  • web/src/systems/loops/lib/loop-runs-view.ts
  • web/src/systems/loops/lib/loop-start-kinds.ts
  • web/src/systems/loops/lib/query-keys.ts
  • web/src/systems/loops/lib/query-options.ts
  • web/src/systems/loops/mocks/fixture-editor-lifecycle.ts
  • web/src/systems/loops/mocks/fixture-hero-path.ts
  • web/src/systems/loops/mocks/fixtures.ts
  • web/src/systems/loops/mocks/handlers.ts
  • web/src/systems/loops/mocks/index.ts
  • web/src/systems/loops/mocks/lint-definition.ts
  • web/src/systems/loops/types.ts
  • web/src/systems/os/apps/loops/__tests__/loop-runs-location.test.tsx
  • web/src/systems/os/apps/loops/loop-run-detail-location.tsx
  • web/src/systems/os/apps/loops/loop-run-form-location.tsx
  • web/src/systems/os/apps/loops/loop-runs-location.tsx
  • web/src/systems/os/apps/loops/loops-catalog-location.tsx
  • web/src/systems/os/apps/loops/use-loop-node-controls.ts
  • web/src/systems/os/apps/loops/use-loop-run-form-page.ts
  • web/src/systems/os/apps/loops/use-loop-run-page.ts
  • web/src/systems/os/apps/loops/use-loop-runs-route.ts
  • web/src/systems/os/apps/loops/use-loops-catalog.ts
  • web/src/systems/os/index.ts
💤 Files with no reviewable changes (1)
  • web/src/systems/os/apps/loops/use-loops-catalog.ts
🚧 Files skipped from review as they are similar to previous changes (216)
  • internal/loop/control_namespace.go
  • internal/loop/control_file_import.go
  • internal/loop/coordinator_metadata.go
  • web/src/systems/loops/mocks/fixture-hero-path.ts
  • internal/loop/control_gate.go
  • internal/loop/control_input.go
  • internal/loop/control_readiness.go
  • internal/loop/action_runagent.go
  • internal/store/globaldb/global_db_task_coordinator_progress.go
  • web/src/systems/loops/lib/tests/loop-list-filters.test.ts
  • web/src/systems/loops/components/detail/loop-recent-runs.tsx
  • internal/loop/control_namespace_history_test.go
  • web/src/systems/loops/components/tests/loop-editor-field.test.tsx
  • web/src/systems/os/apps/loops/tests/loop-runs-location.test.tsx
  • internal/loop/generation_snapshot.go
  • internal/api/core/handlers_agents.go
  • internal/loop/watch_events_registry_phase_a.go
  • internal/loop/coordinator_generation.go
  • web/src/systems/loops/hooks/tests/use-loops.test.tsx
  • internal/daemon/scheduler_loop_coordinator.go
  • internal/loop/coordinator_wait.go
  • internal/store/globaldb/global_db_goal_binding_activation_owner.go
  • web/src/systems/loops/lib/tests/loop-catalog.test.ts
  • internal/api/contract/loops.go
  • web/src/systems/loops/adapters/loops-api-errors.ts
  • internal/daemon/loop_action_policy.go
  • internal/api/spec/schema_customizers.go
  • web/src/systems/loops/components/stories/loop-run-scenario-props.ts
  • internal/loop/action_internal_test.go
  • web/src/systems/loops/components/catalog/loop-catalog.tsx
  • web/src/systems/loops/components/editor/loop-editor-toolbar.tsx
  • web/src/systems/loops/components/stories/loop-run-scenario-types.ts
  • internal/api/core/loops_test.go
  • web/src/systems/loops/components/editor/loop-editor-contract.tsx
  • web/src/systems/loops/hooks/loop-editor-store-draft.ts
  • internal/loop/service_pinned_definition.go
  • web/src/systems/loops/lib/tests/loop-events.test.ts
  • web/src/systems/loops/components/run-page/loop-run-story-timeline.tsx
  • web/src/systems/loops/lib/loop-run-story.ts
  • web/src/systems/loops/components/detail/loop-declared-inputs.tsx
  • web/src/systems/loops/lib/tests/loop-node-schema.test.ts
  • internal/daemon/native_create_tools.go
  • web/src/systems/os/index.ts
  • internal/api/core/agent_definition_resolution.go
  • internal/loop/coordinator_error_routes.go
  • web/src/systems/loops/components/run-page/loop-run-progress-panel.tsx
  • internal/store/globaldb/global_db_goal_session_cleanup.go
  • internal/loop/coordinator_succession.go
  • internal/loop/generation_output_runtime.go
  • internal/daemon/daemon_review_and_fix_e2e_integration_test.go
  • web/src/systems/loops/mocks/index.ts
  • web/src/systems/os/apps/loops/use-loop-run-form-page.ts
  • internal/api/spec/loop_schemas.go
  • internal/loop/coordinator.go
  • web/src/systems/loops/lib/tests/loop-reliability.test.ts
  • web/src/systems/loops/components/tests/loop-detail.test.tsx
  • web/src/systems/loops/hooks/use-loop-node-inventory.ts
  • web/src/systems/loops/components/run-form/loop-run-ways-to-start.tsx
  • internal/store/globaldb/global_db_goal_binding_activation_cleanup.go
  • internal/api/contract/loop_lifecycle.go
  • internal/loop/control_namespace_history.go
  • web/src/systems/loops/lib/tests/loop-runs-view.test.ts
  • web/src/systems/loops/adapters/loops-api.ts
  • web/src/systems/loops/components/detail/loop-versions-panel.tsx
  • internal/daemon/loop_managed_binding.go
  • web/src/systems/loops/components/stories/loop-run-lifecycle-fixtures.ts
  • web/src/systems/loops/lib/loop-node-schema-types.ts
  • web/src/systems/loops/components/detail/loop-start-bindings-panel.tsx
  • internal/loop/coordinator_terminal_helpers.go
  • web/src/systems/loops/components/editor/loop-editor.tsx
  • web/src/systems/loops/lib/loop-catalog-presentation.ts
  • internal/loop/coordinator_outputs.go
  • web/src/systems/loops/components/run-page/loop-run-now-card.tsx
  • internal/api/contract/contract_test.go
  • web/src/systems/loops/components/run-page/loop-run-controls.tsx
  • web/src/systems/loops/components/editor/loop-editor-publish-rejected-strip.tsx
  • web/src/systems/loops/components/editor/loop-editor-field-effects.tsx
  • internal/store/globaldb/global_db_goal_checkpoint_integration_test.go
  • web/src/systems/loops/lib/query-keys.ts
  • web/src/systems/loops/components/run-form/loop-run-form.tsx
  • web/src/systems/loops/components/tests/loop-editor.test.tsx
  • packages/ui/src/components/custom/confirm-dialog.tsx
  • web/src/systems/loops/components/tests/loop-catalog.test.tsx
  • web/src/systems/loops/components/tests/loop-run-page.test.tsx
  • web/src/systems/loops/components/editor/loop-editor-field.tsx
  • web/src/systems/loops/lib/loop-overrides.ts
  • web/src/systems/loops/components/tests/loop-linter-dock.test.tsx
  • internal/loop/action_types.go
  • web/src/systems/loops/components/run-form/loop-run-preview.tsx
  • web/src/systems/loops/components/detail/loop-stats-panel.tsx
  • web/src/systems/loops/components/tests/loop-run-overrides.test.tsx
  • web/src/systems/loops/lib/loop-run-progress.ts
  • internal/loop/control_plan_branch.go
  • internal/loop/action_runtime_helpers.go
  • web/src/systems/loops/lib/loop-run-page-view.ts
  • web/src/systems/loops/lib/loop-node-fields.ts
  • web/src/systems/loops/types.ts
  • web/src/systems/loops/components/run-page/loop-run-outcome-card.tsx
  • web/src/systems/loops/lib/loop-node-verb-copy.ts
  • web/src/systems/loops/lib/loop-node-lifecycle.ts
  • web/src/systems/loops/lib/query-options.ts
  • web/src/systems/loops/lib/tests/loop-run-progress.test.ts
  • web/src/systems/loops/components/catalog/loop-catalog-lede.tsx
  • web/src/systems/loops/lib/loop-palette.ts
  • internal/loop/control_tasks.go
  • internal/store/globaldb/global_db_task_coordinator.go
  • web/src/systems/loops/hooks/use-loop-editor-state.ts
  • internal/loop/coordinator_lifecycle_test.go
  • web/src/systems/loops/components/loop-page-lede.tsx
  • internal/loop/service_node_requeue.go
  • internal/loop/coordinator_action.go
  • internal/loop/fanout_materialization.go
  • internal/daemon/native_create_tools_test.go
  • internal/store/globaldb/queries/loop_runtime.sql
  • web/src/systems/loops/components/run-page/loop-node-row-actions.tsx
  • web/src/systems/loops/hooks/use-loop-node-actions.ts
  • web/src/systems/loops/index.ts
  • web/src/systems/loops/adapters/loops-runs-api.ts
  • web/src/systems/loops/lib/tests/loop-editor-lint.test.ts
  • web/src/systems/loops/components/catalog/loop-catalog-filters.tsx
  • web/src/systems/loops/mocks/fixture-editor-lifecycle.ts
  • web/src/systems/loops/components/catalog/loop-catalog-row.tsx
  • internal/daemon/loop_api_runs_test.go
  • web/src/systems/loops/adapters/loop-nodes-api.ts
  • web/src/systems/loops/hooks/use-loop-fork.ts
  • web/src/systems/loops/components/editor/loop-linter-dock.tsx
  • internal/api/core/handlers_test.go
  • web/src/systems/loops/components/detail/loop-limits-panel.tsx
  • web/src/systems/loops/components/run-form/loop-run-overrides.tsx
  • web/src/systems/loops/lib/tests/loop-run-story.test.ts
  • internal/loop/coordinator_lifecycle.go
  • web/src/systems/loops/components/detail/loop-detail.tsx
  • web/src/systems/loops/components/run-page/loop-run-parked-panels.tsx
  • web/src/systems/loops/components/run-page/loop-quarantine-sheet.tsx
  • internal/store/globaldb/global_db_loop_outputs.go
  • web/src/systems/loops/components/runs/loop-node-inventory-view.tsx
  • web/src/systems/loops/components/run-page/loop-run-waits-rail.tsx
  • web/src/systems/loops/components/tests/loop-run-form.test.tsx
  • web/src/systems/loops/components/run-form/loop-run-active-notice.tsx
  • web/src/systems/loops/components/editor/loop-editor-sidebar.tsx
  • web/src/systems/os/apps/loops/use-loop-node-controls.ts
  • web/src/systems/loops/lib/loop-editor-definition.ts
  • internal/daemon/daemon_mock_agents_integration_test.go
  • web/src/systems/loops/lib/loop-start-kinds.ts
  • web/src/systems/loops/lib/loop-run-story-types.ts
  • internal/loop/coordinator_control_test.go
  • internal/loop/action_test.go
  • web/src/systems/loops/components/run-page/loop-run-control-dialog.tsx
  • web/src/systems/loops/components/run-page/loop-node-control-menu.tsx
  • web/src/systems/loops/lib/loop-editor-draft.ts
  • web/src/systems/loops/lib/loop-graph.ts
  • web/src/systems/loops/components/editor/loop-editor-wait-mode.tsx
  • web/src/systems/loops/lib/loop-events.ts
  • web/src/systems/loops/lib/loop-contract-terminal-fields.ts
  • web/src/systems/loops/lib/loop-catalog.ts
  • web/src/systems/loops/components/editor/loop-editor-contract-terminals.tsx
  • internal/loop/goal/types_binding.go
  • web/src/systems/loops/components/detail/loop-reliability-panel.tsx
  • web/src/systems/loops/lib/loop-runs-view.ts
  • web/src/systems/os/apps/loops/loops-catalog-location.tsx
  • internal/loop/coordinator_goal_control.go
  • internal/loop/coordinator_test.go
  • web/src/systems/loops/lib/loop-node-inventory.ts
  • web/src/systems/loops/components/tests/loop-start-bindings-panel.test.tsx
  • web/src/systems/loops/components/detail/loop-contract-panel.tsx
  • web/src/systems/loops/components/run-page/loop-run-page-body.tsx
  • web/src/systems/loops/lib/loop-run-story-lifecycle-rows.ts
  • web/src/systems/loops/hooks/use-loop-actions.ts
  • web/src/systems/loops/components/editor/loop-editor-source-strip.tsx
  • web/src/systems/loops/lib/loop-node-wait-fields.ts
  • internal/loop/service_control.go
  • web/src/systems/loops/lib/loop-editor-lint.ts
  • web/src/systems/loops/hooks/tests/use-loop-actions.test.tsx
  • web/src/systems/os/apps/loops/loop-run-detail-location.tsx
  • internal/store/globaldb/sqlcgen/loop_runtime.sql.go
  • web/src/systems/loops/components/loop-rail-section.tsx
  • internal/store/globaldb/global_db_goal_binding_cleanup.go
  • web/src/systems/loops/mocks/fixtures.ts
  • web/src/systems/loops/components/run-page/loop-node-control-dialog.tsx
  • internal/loop/linter_wait.go
  • packages/ui/src/components/tests/confirm-dialog.test.tsx
  • web/src/systems/loops/lib/loop-node-controls.ts
  • internal/loop/control_plan.go
  • web/src/systems/loops/hooks/use-loop-editor.ts
  • internal/loop/coordinator_history.go
  • web/src/systems/loops/components/editor/loop-editor-inspector.tsx
  • web/src/systems/loops/components/editor/loop-editor-effects.tsx
  • internal/store/globaldb/global_db_goal_binding_transitions.go
  • web/src/systems/loops/components/run-page/loop-run-overflow-menu.tsx
  • web/src/systems/loops/mocks/handlers.ts
  • internal/daemon/scheduler_runtime_test.go
  • web/src/systems/loops/components/stories/loop-run-page-fixtures.ts
  • internal/store/globaldb/global_db_goal_turn_runtime_integration_test.go
  • internal/loop/output_ref.go
  • web/src/systems/loops/components/editor/loop-editor-json-field.tsx
  • web/src/systems/os/apps/loops/use-loop-runs-route.ts
  • internal/loop/coordinator_watch.go
  • internal/loop/linter_test.go
  • web/src/systems/loops/components/run-form/loop-run-after-start.tsx
  • internal/loop/node_attempt.go
  • web/src/systems/loops/lib/loop-node-schema.ts
  • internal/loop/coordinator_watch_events.go
  • web/src/systems/loops/lib/loop-list-filters.ts
  • internal/store/globaldb/global_db_task_coordinator_runtime.go
  • web/src/systems/loops/hooks/loop-editor-store-contracts.ts
  • web/src/systems/loops/lib/loop-node-lifecycle-fields.ts
  • web/src/systems/loops/lib/loop-quarantine-entry.ts
  • web/src/systems/loops/lib/tests/loop-editor-draft.test.ts
  • web/src/systems/loops/components/catalog/loop-catalog-card.tsx
  • web/src/systems/loops/components/editor/loop-editor-canvas.tsx
  • web/src/systems/os/apps/loops/loop-run-form-location.tsx
  • web/src/systems/os/apps/loops/loop-runs-location.tsx
  • web/src/systems/os/apps/loops/use-loop-run-page.ts
  • web/src/systems/loops/hooks/loop-editor-store-publish.ts
  • web/src/systems/loops/lib/loop-node-now-view.ts
  • internal/loop/service_test.go

Comment on lines +79 to +82
const expires = firstDefined(waits.map(node => node.waitExpiresAfter));
const expiry = expires
? ` A wait gives up after ${formatLoopDuration(expires)} and takes its fallback path instead of waiting forever.`
: "";

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not state that every expiry takes a fallback path.

An expiry can contain escalate effects without an expiry route. The current text then reports a fallback path that does not exist. Use neutral expiry text unless the graph exposes a configured route.

Proposed change
-    ? ` A wait gives up after ${formatLoopDuration(expires)} and takes its fallback path instead of waiting forever.`
+    ? ` A wait expires after ${formatLoopDuration(expires)} instead of waiting forever.`
📝 Committable suggestion

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

Suggested change
const expires = firstDefined(waits.map(node => node.waitExpiresAfter));
const expiry = expires
? ` A wait gives up after ${formatLoopDuration(expires)} and takes its fallback path instead of waiting forever.`
: "";
const expires = firstDefined(waits.map(node => node.waitExpiresAfter));
const expiry = expires
? ` A wait expires after ${formatLoopDuration(expires)} instead of waiting forever.`
: "";
🤖 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 `@web/src/systems/loops/lib/loop-reliability.ts` around lines 79 - 82, Update
the expiry message construction near `expires` and `expiry` so it only mentions
a fallback path when the graph exposes a configured expiry route; otherwise use
neutral wording that describes the wait timeout without claiming a fallback.
Preserve the existing duration formatting and behavior when no expiry is
present.

Comment on lines +29 to +49
const DURATION_PATTERN = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/;
const DURATION_UNIT_MS: Record<string, number> = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 };

type RawRecord = Record<string, unknown>;

function record(value: unknown): RawRecord | null {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as RawRecord)
: null;
}

function text(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}

/** Parses the Go duration subset the lifecycle keys use; `null` when unparseable. */
function durationMs(raw: string): number | null {
const match = DURATION_PATTERN.exec(raw);
if (!match) return null;
return Number(match[1]) * DURATION_UNIT_MS[match[2]];
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the daemon linter and its duration parsing contract.
fd -a '^linter_lifecycle\.go$' . -x ast-grep outline {} --items all
rg -n -C 4 'time\.ParseDuration|timeout_exceeds_deadline|lintLifecycleNode' \
  --glob 'linter_lifecycle.go'

Repository: compozy/compozy

Length of output: 1095


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the daemon duration parser and surrounding lifecycle duration references.
sed -n '1,120p' internal/loop/linter_lifecycle.go

echo '--- timeout_exceeds_deadline references ---'
rg -n -C 6 'timeout_exceeds_deadline|deadline|timeout' internal/loop/linter_lifecycle.go || true

echo '--- mock duration parser area ---'
sed -n '1,80p' web/src/systems/loops/mocks/lint-definition.ts

echo '--- mock timeout_exceeds_deadline references ---'
rg -n -C 6 'timeout_exceeds_deadline|durationMs|deadline|timeout' web/src/systems/loops/mocks/lint-definition.ts || true

echo '--- all durationMs usages ---'
rg -n 'durationMs\(' web/src/systems/loops/mocks/lint-definition.ts || true

Repository: compozy/compozy

Length of output: 11032


Use Go-compatible duration parsing for lifecycle fields.

The daemon parses timeout/deadline values with time.ParseDuration, which accepts composite durations such as 1m30s, while durationMs("1m30s") returns null. If either field is composite, lintNodeLifecycle skips timeout_exceeds_deadline and can emit duration_invalid instead of validating the ordering.

🧰 Tools
🪛 OpenGrep (1.26.0)

[ERROR] 46-46: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 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 `@web/src/systems/loops/mocks/lint-definition.ts` around lines 29 - 49, Update
durationMs to parse the full Go-compatible duration syntax used by
time.ParseDuration, including composite values such as 1m30s, while preserving
null for invalid input. Ensure lintNodeLifecycle can validate
timeout_exceeds_deadline for composite timeout and deadline fields instead of
treating them as duration_invalid.

Source: Coding guidelines

Comment on lines +92 to +93
const emitInvalid = hasEmit && (text(emit?.kind) === "" || record(effect?.with) !== null);
if (hasEmit === hasTool || emitInvalid) {

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject every with key on an emit.

Line 92 ignores with when its value is a string, number, boolean, array, or null. For example, { emit: { kind: "x" }, with: null } produces no issue. The documented contract says an emit never carries with.

Proposed change
     const hasEmit = emit !== null;
     const hasTool = tool !== "";
-    const emitInvalid = hasEmit && (text(emit?.kind) === "" || record(effect?.with) !== null);
+    const hasWith = effect !== null && Object.prototype.hasOwnProperty.call(effect, "with");
+    const emitInvalid = hasEmit && (text(emit?.kind) === "" || hasWith);
📝 Committable suggestion

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

Suggested change
const emitInvalid = hasEmit && (text(emit?.kind) === "" || record(effect?.with) !== null);
if (hasEmit === hasTool || emitInvalid) {
const hasEmit = emit !== null;
const hasTool = tool !== "";
const hasWith = effect !== null && Object.prototype.hasOwnProperty.call(effect, "with");
const emitInvalid = hasEmit && (text(emit?.kind) === "" || hasWith);
if (hasEmit === hasTool || emitInvalid) {
🤖 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 `@web/src/systems/loops/mocks/lint-definition.ts` around lines 92 - 93, Update
emitInvalid in the lint-definition validation so any present effect.with key is
rejected for emits, regardless of its value or type, including null. Preserve
the existing invalid-kind and hasEmit/hasTool validation behavior.

loadedCount={inventory.loadedCount}
loopFilter={search.nodes_loop ?? ""}
loopOptions={[...new Set(runs.map(run => run.loop_name))].sort()}
nowMs={Date.now()}

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.

React Doctor · react-hooks-js/purity (error)

This component misses React Compiler's automatic memoization & re-renders more than it should: Cannot call impure function during render. Rewrite the flagged code so the compiler can optimize it.

Fix → Date.now is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).

Docs

@pedronauck

Copy link
Copy Markdown
Member Author

Superseded by the single final Loop node lifecycle PR now being assembled from the complete three-stack implementation. The existing CodeRabbit review was preserved locally and its findings are being applied to the final branch.

This branch was successfully deployed

1 active deployment
Preview — f542f2a4 Deployed Aug 4, 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