Repository navigation
feat: complete Loop lifecycle Web, docs, and QA (stack 3/3) - #302
pedronauck wants to merge 10 commits into
Conversation
|
Too many files changed for review. ( Bypass the limit by tagging |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (8)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughThis 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. ChangesLoop platform lifecycle changes
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
React Doctor found 1 new issue in 1 file · 1 error · score 84 / 100 (Needs work) · 0 fixed · vs Errors
Reviewed by React Doctor for commit |
There was a problem hiding this comment.
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 winFix the "Waiting on you" row: missing
nodeIdand possible label mismatch.Two issues in this block:
"Needs attention"and"Quarantined"attach the single node's id when their count is 1 (lines 35, 40)."Waiting on you"does not, even whenwaiting === 1, thoughwaitingis computed the same way. This is an inconsistency in the row-building logic.waitingcounts every node with any open wait, includingtimerwaits. Inloop-run-parked-panels.tsx, thetimerwait 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 winAlign
age_secondswithcreated_atin the wait fixtures.The
wait()factory setscreated_at: minutesAgo(18). Two overrides changeage_secondsbut keep that default. The timer wait declares 360 seconds (6 minutes) and the approval wait declares 22 minutes. A component that renderscreated_atand a component that rendersage_secondswill then show different ages in the same capture. Overridecreated_attogether withage_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 winGuard the formatted time before adding the
atclause.
clockTimereturns""whenrequested_atis 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 textat. 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 winAdd unit/e2e coverage for the requeue gating rule.
The
LoopQuarantineSheetonly usesnode.quarantinedfor theloop-quarantine-requeuebutton, and existing Stories/rgoutput 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 winDo not present the first node limits as one loop-wide policy.
firstDefinedcan selecttimeoutfrom one node anddeadlinefrom 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 winRead wait expiry from
params.expires.Wait controls store their configuration in
params. The projection reads root-levelexpires, so an authored expiry never reacheswaitTile. The test fixture repeats this invalid shape and hides the defect.
web/src/systems/loops/lib/loop-graph.ts#L101-L104: readafterfromrecord.params.expires.web/src/systems/loops/lib/__tests__/loop-reliability.test.ts#L50-L59: create the wait expiry underparamsand 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 winShow 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 literal422also 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 winAlign the fallback order for kind and reference.
kindprefersstarted_origin_kind, whilerefprefersstarted_by_ref. When a run carries both pairs and they differ, the line pairs the origin kind with the actor reference, for exampleschedule · 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 winExpose
thisFormDeclaredin the run-form UI or remove it.
LoopRunWaysToStartonly displaysthisFormandalsoDeclared, while every run post is alwayshttp. Addkind/httpto this form whenthisFormDeclaredis 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 winUse
query.isLoadingfor the inventory skeleton state.When no inventory state is selected, the query is disabled and v5 keeps
status: "pending"withfetchStatus: "idle", soquery.isPendingis true while no request runs. Consumers that render a loading view fromisLoadingcan show it permanently in that state; usequery.isLoading, which isisPending && 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 winThe
errorprop is declared but the only consumer does not pass it.The doc for
errorstates that the daemon's rejection is shown inline. Inweb/src/systems/os/apps/loops/loop-run-detail-location.tsx,LoopRunControlDialogreceivesgeneration,isPending,onConfirm,onOpenChange,runId,status, andverb, but noerror.LoopNodeControlDialogin the same file does receiveerror={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
errorat 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 winDo not state an episode number when the payload carries no quarantine entry.
(node.quarantineEntry?.episodes.length ?? 1) + 1renders "episode 2" whenquarantineEntryis absent. The payload never reported an episode in that case.loopNodeStateStripuses?? 0for 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 winSeparate the raw cause from the following sentence.
causecomes straight from the payload'sfailure.causeand is usually a machine token, for exampleconnection_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 winUse the requeue icon when the promoted verb is
requeue.The promoted button always renders
Play.LoopNodeControlMenurendersRotateCcwfor the samerequeueverb. 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 winDerive the parked noun from the fan-out node, not from every parked node.
parkedNounscans 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 === 2and the meta line says "parked" instead of "paused".latestFanOutalready resolveswidestId, 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 winFormat the timestamps before showing them to operators.
waitReasonprintswait.resume_atandretryReasonprintsoutput.next_attempt_atdirectly. These fields are raw API timestamps, so the row reads liketimer — 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 winTreat an empty
causeas absent.
cause ?? failure?.codekeeps an empty string. If the terminal frame carriescause: "",causeLabelbecomes"", the segment is dropped, and the availablefailure.codeis 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 winSource
loopOptionsfrom the loop catalog before building the inventory view.
LoopNodeInventoryViewProps.loopOptionssays the filter comes from the workspace catalog, but this passes only loop names from loaded runs. IfrunsQueryloads 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 winUse distinct icons for "retry" and "requeued".
retryandrequeuedboth map toRotateCcw. 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 valueConsider reusing
generationOutputRefForPayloadhere.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 ininternal/loop/coordinator_watch_events.goLine 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
setGenerationOutputRefclearsruntimePayload.🤖 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 winAccept a typed reader instead of
any.
generationOutputRuntimeViewtakesreader anyand 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 whenruntimePayloadis 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 winExport one shared
WAIT_MODESlist next toLoopWaitMode.The same vocabulary is declared twice as a value:
web/src/systems/loops/lib/loop-node-wait-fields.tsLine 14 andweb/src/systems/loops/components/editor/loop-editor-wait-mode.tsxLine 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 valueDocument that
runtimePayloadis not serialized.
ExpectedEpochcarries a comment that explains its non-durable role.runtimePayloadhas no comment.encoding/jsonignores 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 valueRemove the duplicate
nodeLifecyclesassignment.Line 40 spreads
view, which already containsnodeLifecycles. 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 valueState the mutual exclusion on
CellFencein the contract.
validateBindingActivationOwnerrejects a request that setsCellFencetogether withCheckpointKey,ExpectedControlEpoch, orGrantID. That rule is only visible in the store package. Add a doc comment onCellFencethat 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 valueConsider one source of truth for "destructive".
Two definitions now describe destructive verbs.
DESTRUCTIVE_VERBShere controls placement below the separator and includescancelandkill.LOOP_NODE_VERB_PRESENTATIONcontrols the danger styling and marks onlykillasdestructive: 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 winDuplicated
LoopNodeLifecyclefixtures 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 passstate,paused, andquarantinedas 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 winGuard the order assertions against missing elements.
indexOfreturns-1for 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 winNote: this component does not resync
textwhenvaluechanges without a remount.
textis derived fromvalueonly at mount, through the lazyuseStateinitializer. If a consumer changesvaluefor the same component instance (nokeychange), the textarea keeps showing the old text. This is the mechanism behind the stale-JSON issue flagged inloop-editor-effects.tsxfor 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 underlyingvalue(for example, a JSON field bound to a node field that changes without aselectionKeyremount), 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
| 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) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
📐 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.
| 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
| 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 { |
There was a problem hiding this comment.
🗄️ 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/loopRepository: 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 -nRepository: 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/loopRepository: 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.
| <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> |
There was a problem hiding this comment.
🎯 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.
| <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.
| case "effect_results": { | ||
| const result = parseEffectResult(frame, payload); | ||
| if (result) { | ||
| next.effectResults = [...state.effectResults, result].slice(-MAX_EFFECT_RESULTS); | ||
| } | ||
| break; | ||
| } |
There was a problem hiding this comment.
🗄️ 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_resultsappends the samedelivery_idagain, so the Inspect surface lists one delivery several times.- A replayed
node_retry_scheduledthat arrives afternode_resumedreinstates 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.
| 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}`, | ||
| }; |
There was a problem hiding this comment.
🗄️ 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.
| 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".
| 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; | ||
| } |
There was a problem hiding this comment.
🗄️ 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 -200Repository: 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 -300Repository: 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 -300Repository: 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.
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]>
4cdbea0 to
b067b70
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
web/src/systems/loops/lib/loop-reliability.ts (1)
1-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse 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
⛔ Files ignored due to path filters (92)
docs/_memory/glossary.mdis excluded by!**/*.mddocs/qa/automation-backlog/all-run-status-seeds.mdis excluded by!**/*.mddocs/qa/bugs/BUG-20260803-agent-workspace-id-disagrees.mdis excluded by!**/*.mddocs/qa/bugs/BUG-20260803-cross-origin-coordinator-duplicate.mdis excluded by!**/*.mddocs/qa/bugs/BUG-20260803-disabled-extension-blocks-wait-resume.mdis excluded by!**/*.mddocs/qa/bugs/BUG-20260803-error-route-fallback-blocked.mdis excluded by!**/*.mddocs/qa/bugs/BUG-20260803-loop-boot-active-coordinator-lease.mdis excluded by!**/*.mddocs/qa/bugs/BUG-20260803-wait-event-rejected-too-late.mdis excluded by!**/*.mddocs/qa/charters/CH-004.mdis excluded by!**/*.mddocs/qa/charters/CH-011.mdis excluded by!**/*.mddocs/qa/charters/CH-022.mdis excluded by!**/*.mddocs/qa/charters/CH-023.mdis excluded by!**/*.mddocs/qa/charters/CH-024.mdis excluded by!**/*.mddocs/qa/charters/CH-026.mdis excluded by!**/*.mddocs/qa/charters/CH-agent-loop-lifecycle-parity.mdis excluded by!**/*.mddocs/qa/charters/CH-approver-loop-wait.mdis excluded by!**/*.mddocs/qa/charters/CH-author-loop-failure-contract.mdis excluded by!**/*.mddocs/qa/charters/CH-operator-lifecycle-ui.mdis excluded by!**/*.mddocs/qa/charters/CH-operator-loop-recovery.mdis excluded by!**/*.mddocs/qa/journeys/J-03-observe-and-approve.mdis excluded by!**/*.mddocs/qa/journeys/J-07-agent-operated-run.mdis excluded by!**/*.mddocs/qa/journeys/J-16-watch-events-wake.mdis excluded by!**/*.mddocs/qa/journeys/J-bound-runaway-work.mdis excluded by!**/*.mddocs/qa/journeys/J-recover-loop-node-failure.mdis excluded by!**/*.mddocs/qa/journeys/J-resume-dead-loop-node.mdis excluded by!**/*.mddocs/qa/personas.mdis excluded by!**/*.mddocs/qa/reports/2026-08-03-loop-node-lifecycle-plan.mdis excluded by!**/*.mddocs/qa/reports/2026-08-03-loop-node-lifecycle-task08.mdis excluded by!**/*.mddocs/qa/reports/2026-08-03-loop-node-lifecycle.mdis excluded by!**/*.mddocs/qa/scenarios/LP-016.mdis excluded by!**/*.mddocs/qa/scenarios/LP-026.mdis excluded by!**/*.mddocs/qa/scenarios/LP-040.mdis excluded by!**/*.mddocs/qa/scenarios/LP-041.mdis excluded by!**/*.mddocs/qa/scenarios/LP-042.mdis excluded by!**/*.mddocs/qa/scenarios/LP-044.mdis excluded by!**/*.mddocs/qa/scenarios/LP-047.mdis excluded by!**/*.mddocs/qa/scenarios/LP-048.mdis excluded by!**/*.mddocs/qa/scenarios/LP-049.mdis excluded by!**/*.mddocs/qa/scenarios/LP-050.mdis excluded by!**/*.mddocs/qa/scenarios/LP-agent-operates-lifecycle-via-native-tools.mdis excluded by!**/*.mddocs/qa/scenarios/LP-approval-link-journey.mdis excluded by!**/*.mddocs/qa/scenarios/LP-cancel-vs-kill.mdis excluded by!**/*.mddocs/qa/scenarios/LP-catalog-runform-walk.mdis excluded by!**/*.mddocs/qa/scenarios/LP-crash-death-resume.mdis excluded by!**/*.mddocs/qa/scenarios/LP-days-long-node-no-clock.mdis excluded by!**/*.mddocs/qa/scenarios/LP-duplicate-event-suppressed.mdis excluded by!**/*.mddocs/qa/scenarios/LP-durable-wait-restart.mdis excluded by!**/*.mddocs/qa/scenarios/LP-editor-authoring-walk.mdis excluded by!**/*.mddocs/qa/scenarios/LP-error-route-fallback.mdis excluded by!**/*.mddocs/qa/scenarios/LP-live-pause-repair-resume.mdis excluded by!**/*.mddocs/qa/scenarios/LP-live-run-survives-extension-disable.mdis excluded by!**/*.mddocs/qa/scenarios/LP-loop-run-deep-link.mdis excluded by!**/*.mddocs/qa/scenarios/LP-on-error-notification-with-context.mdis excluded by!**/*.mddocs/qa/scenarios/LP-operator-lifecycle-ui.mdis excluded by!**/*.mddocs/qa/scenarios/LP-quarantine-diagnose-requeue.mdis excluded by!**/*.mddocs/qa/scenarios/LP-run-detail-story-redesign.mdis excluded by!**/*.mddocs/qa/scenarios/LP-sick-target-degrades-one-lane.mdis excluded by!**/*.mddocs/qa/scenarios/LP-terminal-outcome-notification.mdis excluded by!**/*.mddocs/qa/scenarios/LP-transient-blip-heals.mdis excluded by!**/*.mddocs/qa/scenarios/LP-unannotated-escalation.mdis excluded by!**/*.mddocs/qa/scenarios/LP-wait-event-catalog-validation.mdis excluded by!**/*.mddocs/qa/scenarios/LP-waiting-inventory-escalation.mdis excluded by!**/*.mddocs/qa/scenarios/TA-070.mdis excluded by!**/*.mddocs/qa/scenarios/TA-076.mdis excluded by!**/*.mddocs/qa/scenarios/TA-084.mdis excluded by!**/*.mddocs/qa/scenarios/TA-action-run-liveness.mdis excluded by!**/*.mddocs/qa/scenarios/TA-loop-failure-breaker.mdis excluded by!**/*.mdopenapi/compozy.jsonis excluded by!**/*.jsonpackages/site/content/docs/configuration/config-toml.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/catalog.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/configure.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/dsl-reference.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/extensions.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/failure-handling.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/guardrails.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/index.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/meta.jsonis excluded by!**/*.jsonpackages/site/content/docs/loops/reference-grammar.mdxis excluded by!**/*.mdxpackages/site/content/docs/loops/running.mdxis excluded by!**/*.mdxpackages/ui/src/components/stories/confirm-dialog.stories.tsxis excluded by!**/*.stories.tsxskills/compozy/references/loops.mdis excluded by!**/*.mdweb/e2e/__tests__/loops.spec.tsis excluded by!web/e2e/**web/e2e/__tests__/os-shell.spec.tsis excluded by!web/e2e/**web/src/generated/compozy-openapi.d.tsis excluded by!**/generated/**,!**/generated/**,!**/*.d.tsweb/src/systems/loops/components/stories/loop-catalog.stories.tsxis excluded by!**/*.stories.tsxweb/src/systems/loops/components/stories/loop-detail.stories.tsxis excluded by!**/*.stories.tsxweb/src/systems/loops/components/stories/loop-editor.stories.tsxis excluded by!**/*.stories.tsxweb/src/systems/loops/components/stories/loop-node-controls.stories.tsxis excluded by!**/*.stories.tsxweb/src/systems/loops/components/stories/loop-node-inventory.stories.tsxis excluded by!**/*.stories.tsxweb/src/systems/loops/components/stories/loop-quarantine-sheet.stories.tsxis excluded by!**/*.stories.tsxweb/src/systems/loops/components/stories/loop-run-form.stories.tsxis excluded by!**/*.stories.tsxweb/src/systems/loops/components/stories/loop-run-page.stories.tsxis excluded by!**/*.stories.tsx
📒 Files selected for processing (223)
internal/api/contract/contract_test.gointernal/api/contract/loop_lifecycle.gointernal/api/contract/loops.gointernal/api/core/agent_create.gointernal/api/core/agent_definition_resolution.gointernal/api/core/handlers_agents.gointernal/api/core/handlers_test.gointernal/api/core/loops_test.gointernal/api/spec/loop_schemas.gointernal/api/spec/schema_customizers.gointernal/daemon/daemon_mock_agents_integration_test.gointernal/daemon/daemon_review_and_fix_e2e_integration_test.gointernal/daemon/loop_action_policy.gointernal/daemon/loop_api_runs_test.gointernal/daemon/loop_managed_binding.gointernal/daemon/native_create_tools.gointernal/daemon/native_create_tools_test.gointernal/daemon/scheduler_loop_coordinator.gointernal/daemon/scheduler_runtime_test.gointernal/loop/action_internal_test.gointernal/loop/action_runagent.gointernal/loop/action_runtime_helpers.gointernal/loop/action_test.gointernal/loop/action_types.gointernal/loop/control_file_import.gointernal/loop/control_gate.gointernal/loop/control_input.gointernal/loop/control_namespace.gointernal/loop/control_namespace_history.gointernal/loop/control_namespace_history_test.gointernal/loop/control_plan.gointernal/loop/control_plan_branch.gointernal/loop/control_readiness.gointernal/loop/control_tasks.gointernal/loop/coordinator.gointernal/loop/coordinator_action.gointernal/loop/coordinator_control_test.gointernal/loop/coordinator_error_routes.gointernal/loop/coordinator_generation.gointernal/loop/coordinator_goal_control.gointernal/loop/coordinator_history.gointernal/loop/coordinator_lifecycle.gointernal/loop/coordinator_lifecycle_test.gointernal/loop/coordinator_metadata.gointernal/loop/coordinator_outputs.gointernal/loop/coordinator_succession.gointernal/loop/coordinator_terminal_helpers.gointernal/loop/coordinator_test.gointernal/loop/coordinator_wait.gointernal/loop/coordinator_watch.gointernal/loop/coordinator_watch_events.gointernal/loop/fanout_materialization.gointernal/loop/generation_output_runtime.gointernal/loop/generation_snapshot.gointernal/loop/goal/types_binding.gointernal/loop/linter_test.gointernal/loop/linter_wait.gointernal/loop/node_attempt.gointernal/loop/output_ref.gointernal/loop/service_control.gointernal/loop/service_node_requeue.gointernal/loop/service_pinned_definition.gointernal/loop/service_test.gointernal/loop/watch_events_registry_phase_a.gointernal/store/globaldb/global_db_goal_binding_activation_cleanup.gointernal/store/globaldb/global_db_goal_binding_activation_owner.gointernal/store/globaldb/global_db_goal_binding_cleanup.gointernal/store/globaldb/global_db_goal_binding_integration_test.gointernal/store/globaldb/global_db_goal_binding_transitions.gointernal/store/globaldb/global_db_goal_checkpoint_integration_test.gointernal/store/globaldb/global_db_goal_session_cleanup.gointernal/store/globaldb/global_db_goal_turn_runtime_integration_test.gointernal/store/globaldb/global_db_loop_outputs.gointernal/store/globaldb/global_db_task_claim_test.gointernal/store/globaldb/global_db_task_coordinator.gointernal/store/globaldb/global_db_task_coordinator_progress.gointernal/store/globaldb/global_db_task_coordinator_runtime.gointernal/store/globaldb/queries/loop_runtime.sqlinternal/store/globaldb/sqlcgen/loop_runtime.sql.gopackages/ui/src/components/__tests__/confirm-dialog.test.tsxpackages/ui/src/components/custom/confirm-dialog.tsxweb/src/systems/loops/adapters/loop-nodes-api.tsweb/src/systems/loops/adapters/loops-api-errors.tsweb/src/systems/loops/adapters/loops-api.tsweb/src/systems/loops/adapters/loops-runs-api.tsweb/src/systems/loops/components/__tests__/loop-catalog.test.tsxweb/src/systems/loops/components/__tests__/loop-detail.test.tsxweb/src/systems/loops/components/__tests__/loop-editor-field.test.tsxweb/src/systems/loops/components/__tests__/loop-editor.test.tsxweb/src/systems/loops/components/__tests__/loop-linter-dock.test.tsxweb/src/systems/loops/components/__tests__/loop-run-form.test.tsxweb/src/systems/loops/components/__tests__/loop-run-overrides.test.tsxweb/src/systems/loops/components/__tests__/loop-run-page.test.tsxweb/src/systems/loops/components/__tests__/loop-start-bindings-panel.test.tsxweb/src/systems/loops/components/catalog/loop-catalog-card.tsxweb/src/systems/loops/components/catalog/loop-catalog-filters.tsxweb/src/systems/loops/components/catalog/loop-catalog-lede.tsxweb/src/systems/loops/components/catalog/loop-catalog-row.tsxweb/src/systems/loops/components/catalog/loop-catalog.tsxweb/src/systems/loops/components/detail/loop-contract-panel.tsxweb/src/systems/loops/components/detail/loop-declared-inputs.tsxweb/src/systems/loops/components/detail/loop-detail.tsxweb/src/systems/loops/components/detail/loop-limits-panel.tsxweb/src/systems/loops/components/detail/loop-recent-runs.tsxweb/src/systems/loops/components/detail/loop-reliability-panel.tsxweb/src/systems/loops/components/detail/loop-start-bindings-panel.tsxweb/src/systems/loops/components/detail/loop-stats-panel.tsxweb/src/systems/loops/components/detail/loop-versions-panel.tsxweb/src/systems/loops/components/editor/loop-editor-canvas.tsxweb/src/systems/loops/components/editor/loop-editor-contract-terminals.tsxweb/src/systems/loops/components/editor/loop-editor-contract.tsxweb/src/systems/loops/components/editor/loop-editor-effects.tsxweb/src/systems/loops/components/editor/loop-editor-field-effects.tsxweb/src/systems/loops/components/editor/loop-editor-field.tsxweb/src/systems/loops/components/editor/loop-editor-inspector.tsxweb/src/systems/loops/components/editor/loop-editor-json-field.tsxweb/src/systems/loops/components/editor/loop-editor-publish-rejected-strip.tsxweb/src/systems/loops/components/editor/loop-editor-sidebar.tsxweb/src/systems/loops/components/editor/loop-editor-source-strip.tsxweb/src/systems/loops/components/editor/loop-editor-toolbar.tsxweb/src/systems/loops/components/editor/loop-editor-wait-mode.tsxweb/src/systems/loops/components/editor/loop-editor.tsxweb/src/systems/loops/components/editor/loop-linter-dock.tsxweb/src/systems/loops/components/loop-page-lede.tsxweb/src/systems/loops/components/loop-rail-section.tsxweb/src/systems/loops/components/run-form/loop-run-active-notice.tsxweb/src/systems/loops/components/run-form/loop-run-after-start.tsxweb/src/systems/loops/components/run-form/loop-run-form.tsxweb/src/systems/loops/components/run-form/loop-run-overrides.tsxweb/src/systems/loops/components/run-form/loop-run-preview.tsxweb/src/systems/loops/components/run-form/loop-run-ways-to-start.tsxweb/src/systems/loops/components/run-page/loop-node-control-dialog.tsxweb/src/systems/loops/components/run-page/loop-node-control-menu.tsxweb/src/systems/loops/components/run-page/loop-node-row-actions.tsxweb/src/systems/loops/components/run-page/loop-quarantine-sheet.tsxweb/src/systems/loops/components/run-page/loop-run-control-dialog.tsxweb/src/systems/loops/components/run-page/loop-run-controls.tsxweb/src/systems/loops/components/run-page/loop-run-now-card.tsxweb/src/systems/loops/components/run-page/loop-run-outcome-card.tsxweb/src/systems/loops/components/run-page/loop-run-overflow-menu.tsxweb/src/systems/loops/components/run-page/loop-run-page-body.tsxweb/src/systems/loops/components/run-page/loop-run-parked-panels.tsxweb/src/systems/loops/components/run-page/loop-run-progress-panel.tsxweb/src/systems/loops/components/run-page/loop-run-story-timeline.tsxweb/src/systems/loops/components/run-page/loop-run-waits-rail.tsxweb/src/systems/loops/components/runs/loop-node-inventory-view.tsxweb/src/systems/loops/components/stories/loop-run-lifecycle-fixtures.tsweb/src/systems/loops/components/stories/loop-run-page-fixtures.tsweb/src/systems/loops/components/stories/loop-run-scenario-props.tsweb/src/systems/loops/components/stories/loop-run-scenario-types.tsweb/src/systems/loops/components/stories/loops-story-shell.tsxweb/src/systems/loops/hooks/__tests__/use-loop-actions.test.tsxweb/src/systems/loops/hooks/__tests__/use-loops.test.tsxweb/src/systems/loops/hooks/loop-editor-store-contracts.tsweb/src/systems/loops/hooks/loop-editor-store-draft.tsweb/src/systems/loops/hooks/loop-editor-store-publish.tsweb/src/systems/loops/hooks/use-loop-actions.tsweb/src/systems/loops/hooks/use-loop-editor-state.tsweb/src/systems/loops/hooks/use-loop-editor.tsweb/src/systems/loops/hooks/use-loop-fork.tsweb/src/systems/loops/hooks/use-loop-node-actions.tsweb/src/systems/loops/hooks/use-loop-node-inventory.tsweb/src/systems/loops/index.tsweb/src/systems/loops/lib/__tests__/loop-catalog.test.tsweb/src/systems/loops/lib/__tests__/loop-editor-draft.test.tsweb/src/systems/loops/lib/__tests__/loop-editor-lint.test.tsweb/src/systems/loops/lib/__tests__/loop-events.test.tsweb/src/systems/loops/lib/__tests__/loop-list-filters.test.tsweb/src/systems/loops/lib/__tests__/loop-node-schema.test.tsweb/src/systems/loops/lib/__tests__/loop-reliability.test.tsweb/src/systems/loops/lib/__tests__/loop-run-progress.test.tsweb/src/systems/loops/lib/__tests__/loop-run-story.test.tsweb/src/systems/loops/lib/__tests__/loop-runs-view.test.tsweb/src/systems/loops/lib/loop-catalog-presentation.tsweb/src/systems/loops/lib/loop-catalog.tsweb/src/systems/loops/lib/loop-contract-terminal-fields.tsweb/src/systems/loops/lib/loop-editor-definition.tsweb/src/systems/loops/lib/loop-editor-draft.tsweb/src/systems/loops/lib/loop-editor-lint.tsweb/src/systems/loops/lib/loop-events.tsweb/src/systems/loops/lib/loop-graph.tsweb/src/systems/loops/lib/loop-list-filters.tsweb/src/systems/loops/lib/loop-node-controls.tsweb/src/systems/loops/lib/loop-node-fields.tsweb/src/systems/loops/lib/loop-node-inventory.tsweb/src/systems/loops/lib/loop-node-lifecycle-fields.tsweb/src/systems/loops/lib/loop-node-lifecycle.tsweb/src/systems/loops/lib/loop-node-now-view.tsweb/src/systems/loops/lib/loop-node-schema-types.tsweb/src/systems/loops/lib/loop-node-schema.tsweb/src/systems/loops/lib/loop-node-verb-copy.tsweb/src/systems/loops/lib/loop-node-wait-fields.tsweb/src/systems/loops/lib/loop-overrides.tsweb/src/systems/loops/lib/loop-palette.tsweb/src/systems/loops/lib/loop-quarantine-entry.tsweb/src/systems/loops/lib/loop-reliability.tsweb/src/systems/loops/lib/loop-run-page-view.tsweb/src/systems/loops/lib/loop-run-progress.tsweb/src/systems/loops/lib/loop-run-story-lifecycle-rows.tsweb/src/systems/loops/lib/loop-run-story-types.tsweb/src/systems/loops/lib/loop-run-story.tsweb/src/systems/loops/lib/loop-runs-view.tsweb/src/systems/loops/lib/loop-start-kinds.tsweb/src/systems/loops/lib/query-keys.tsweb/src/systems/loops/lib/query-options.tsweb/src/systems/loops/mocks/fixture-editor-lifecycle.tsweb/src/systems/loops/mocks/fixture-hero-path.tsweb/src/systems/loops/mocks/fixtures.tsweb/src/systems/loops/mocks/handlers.tsweb/src/systems/loops/mocks/index.tsweb/src/systems/loops/mocks/lint-definition.tsweb/src/systems/loops/types.tsweb/src/systems/os/apps/loops/__tests__/loop-runs-location.test.tsxweb/src/systems/os/apps/loops/loop-run-detail-location.tsxweb/src/systems/os/apps/loops/loop-run-form-location.tsxweb/src/systems/os/apps/loops/loop-runs-location.tsxweb/src/systems/os/apps/loops/loops-catalog-location.tsxweb/src/systems/os/apps/loops/use-loop-node-controls.tsweb/src/systems/os/apps/loops/use-loop-run-form-page.tsweb/src/systems/os/apps/loops/use-loop-run-page.tsweb/src/systems/os/apps/loops/use-loop-runs-route.tsweb/src/systems/os/apps/loops/use-loops-catalog.tsweb/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
| 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.` | ||
| : ""; |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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]]; | ||
| } |
There was a problem hiding this comment.
🗄️ 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 || trueRepository: 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
| const emitInvalid = hasEmit && (text(emit?.kind) === "" || record(effect?.with) !== null); | ||
| if (hasEmit === hasTool || emitInvalid) { |
There was a problem hiding this comment.
🎯 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.
| 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()} |
There was a problem hiding this comment.
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).
|
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. |
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.yamlfilters.Stack and review order
mainReview 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
2. One deterministic failure precedence
3. Durable reactions and effects
4. Long-running supervision
5. Cancel, kill, pause, and resume
canceled(operator_cancel).canceled(operator_kill).stopverb andoperator_stopcause are deleted instead of aliased.6. Durable waits and admission dedupe
7. Target health, quarantine, and requeue
8. Agent and operator manageability
compozy loop, OpenAPI, generated TypeScript, and eightcompozy__loop_*native tools.9. Web operations and truthful state
10. Loop authoring and hero path
@compozy/uiprimitives/tokens rather than parallel Web-only schemas or controls.11. Documentation and bundled skill
skills/compozy/guidance so agents can discover and operate the public lifecycle contract.stoplanguage while leaving dated historical reports unchanged.Storage and rebase notes
main; the originalloop-node-lifecyclebranch and closed feat: loop node lifecycle #294 remain preserved.NOT NULLcolumn.QA evidence
The production-like run is recorded in
docs/qa/reports/2026-08-03-loop-node-lifecycle.md.blocked-verify, 0 untested, 0 fail.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.golinesrun now passes.Gate status
The final
make gate-fullwas 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
stoprun/node verbs, HTTP/UDS routes, CLI/native entries, schemas, UI controls, andoperator_stoptransition cause.cancelfor cooperative drain andkillfor immediate termination; there is no compatibility alias or dual field.watchingas a live run state andcanceledas its own truthful terminal state, bringing the public vocabulary to 12 states.Compozy Impact Audit
skills/compozy/for the new CLI/native lifecycle paths, state vocabulary, cancel/kill semantics, waits, quarantine, and agent-operable recovery flows.Review guide
.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-fullpasses 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
Bug Fixes