Skip to content

fix: preserve Loop template manifests across hydration - #317

Merged
pedronauck merged 1 commit into
mainfrom
codex/issue-313-loop-manifest
Aug 5, 2026
Merged

pedronauck merged 1 commit into
mainfrom
codex/issue-313-loop-manifest

Conversation

@pedronauck

@pedronauck pedronauck commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

Context and reproduction

Issue #313 exposed a mismatch between a Loop definition that could validate/dry-run and the executed-definition snapshot used when a real Run was submitted. A valid parent Loop with two run-loop children, templated child inputs, references to a prior node output, and omitted child mode values compiled successfully. Submission then failed while hydrating the persisted definition with executed definition template manifest changed.

The focused regression reproduced the exact drift:

  • the snapshot built from the authored definition contained 8 template manifest entries;
  • hydration recompiled the persisted, default-folded definition and produced 10 entries;
  • the added entries were nodes.workspace_child.params.mode and nodes.marketplace_child.params.mode, both with source await.

This meant a definition accepted before submission could not cross the build → persistence → hydration boundary required to create and retain a Run.

Proven root cause

Compiler.Compile stored foldDefinitionDefaults(def) in ResolvedDefinition.Definition, but linting and every template/condition compilation phase still consumed the original, unfolded def.

The snapshot therefore combined two different representations:

  1. the persisted definition already contained default child Loop modes;
  2. the template manifest was compiled from the pre-default definition and did not contain those mode keys.

On hydration, the daemon correctly recompiled the persisted definition. The default await strings were then discovered as template sources, so the integrity comparison rejected the snapshot. The integrity check was revealing a real compiler inconsistency; it was not the cause and has not been weakened.

Solution design

  • Fold defaults once at the start of compilation and use that single canonical definition for linting, contract/start/node compilation, watch-event contracts, graph metadata, and child Loop compilation.
  • Make BuildExecutedDefinitionSnapshot validate its own canonical output through the production loader before returning bytes or a digest. A snapshot that cannot round-trip can no longer reach persistence.
  • Exercise that same snapshot boundary during dry-run, after Loop participation resolution/validation and before returning a plan. This preserves the existing network-participation diagnostic precedence while ensuring preview cannot approve a definition that submission would reject.
  • Replace the generic manifest mismatch with deterministic diagnostics that identify the manifest kind, exact key, and source for added, missing, or changed entries.
  • Keep digest verification and manifest equality strict. No fallback, legacy alias, or integrity relaxation was added.

Changes by surface

Runtime and persistence

  • Canonicalized compiler input in internal/loop/compiler.go.
  • Added focused manifest comparison helpers in internal/loop/executed_definition_manifest.go and kept production files below the 500-line limit.
  • Added build-time self-hydration and precise loader diagnostics.
  • Added dry-run snapshot validation at the service boundary.
  • Persisted Run and definition-snapshot storage formats remain unchanged; no schema migration is required.

CLI, HTTP, UDS, native tools, and web

  • compozy loop run --dry-run and compozy__loop_run with dry: true now exercise the same executed-definition snapshot boundary used by a real run.
  • Real submission and fresh Run reads were exercised over CLI/UDS and HTTP.
  • No request/response DTO, OpenAPI, CLI flag, UDS method, native-tool descriptor, schema digest, or web component changed. All public paths inherit the corrected service behavior.
  • The web client needs no code change because it already uses the same HTTP service and no rendered behavior or copy changed.

Documentation and agent operation

  • Updated the Loop authoring docs to explain dry-run snapshot validation and exact manifest diagnostics.
  • Updated the bundled official compozy skill so agents know that dry-run proves snapshot save/load before publication or token spend.
  • Added and completed the content-addressed QA scenario, charter, journey impact, and dated report.

Invariants and canonical test ownership

  1. Invariant: an executed-definition snapshot built from templated parent/child Loops reloads with the identical template manifest, including folded child modes. Owner: Loop snapshot/hydration layer. Canonical suite: internal/loop/coordinator_snapshot_test.go.
  2. Invariant: dry-run returns a plan only after the exact snapshot used by submission can build and load; failure creates no state and names the offending key. Owner: Loop service boundary. Canonical suite: internal/loop/service_test.go.
  3. Invariant: Start persists a workspace-scoped Run and snapshot in SQLite, and the stored definition hydrates with parent input, prior-output, workspace-child, marketplace-child, and default-mode templates intact. Owner: Loop service + global store integration. Canonical suite: internal/loop/service_integration_test.go.

Focused evidence:

  • go test -race ./internal/loop -run 'TestCoordinator...|TestServiceDryRun...|TestServiceParticipation...' -count=1 — 8 tests passed.
  • go test -race -tags=integration ./internal/loop -run 'TestServiceIntegrationExecutedDefinitionSnapshot/...' -count=1 — 2 tests passed.
  • make gate-full — PASS, fingerprint d57067525fcf8439c110d219821c27aa3ff864fb; 20,689 Go tests passed with 2 documented skips, plus codegen, installer, source-size, Bun lint/typecheck/test/build, Go format/lint/build, and package boundaries.

Real-scenario QA and evidence

QA used a fresh isolated runtime with a unique COMPOZY_HOME, HTTP port 52702, UDS path, and extension process envelope.

  • Catalog inspection proved the parent and workspace-child came from the workspace and review-and-fix came from the marketplace.
  • Validation returned valid: true.
  • Two identical dry-runs returned the expected generation plan and left the Runs list empty.
  • A real UDS submission created Run looprun-4b66e31d8f935186 with digest sha256:73a51e3b67426d74ce8a9749973e038819463c549e62f77a83353f42aa1eaf85.
  • Fresh CLI/UDS and HTTP reads returned that same persisted Run and digest.
  • Hydration preserved both child nodes with mode: await, parent-input templates, and prior-node-output templates.
  • The lab had no provider credentials, so the draft node later quarantined after persistence. That is outside this submission/hydration contract and did not affect the proven end state.
  • Strict QA evidence audit: PASS, 0 blockers, 0 warnings.
  • Mandatory teardown: teardown.json reports "clean": true, both registered processes were terminated, and survivors are empty.

Scenario: docs/qa/scenarios/LP-loop-template-snapshot-round-trip.md
Report: docs/qa/reports/2026-08-05-issue-313-loop-manifest.md

Compozy Impact Audit

  • Native tools: Behavioral impact flows through existing compozy__loop_run dry-run/start paths. Checked compozy__loop_validate, compozy__loop_run, compozy__loop_runs, and their CLI/HTTP/UDS fallbacks. No tool IDs, toolsets, descriptors, I/O schemas, schema digests, risk flags, availability diagnostics, or capability gates changed.
  • Extensibility and hooks: Workspace and marketplace child Loop resolution were exercised together. No extension contracts, hooks, capabilities, tool/resource registries, bridge SDKs, MCP sidecars, or config.toml keys/defaults/lifecycle changed; the compiler/service boundary is shared by those sources.
  • Workspace data isolation: Executed definitions and Runs remain workspace-scoped existing data. Integration and QA used explicit workspace IDs for create, snapshot read, Run read, CLI/UDS, and HTTP paths; no global/session/agent datum or cache/SSE/event contract was introduced, and cross-workspace list/read behavior is unchanged.
  • Official Compozy skill: Updated skills/compozy/references/loops.md to document the dry-run round-trip guarantee and exact key/source diagnostics.

Compatibility, risks, and decisions

  • This is a greenfield consistency fix with no legacy compatibility bridge. Existing inconsistent snapshots remain rejected; the rejection is now earlier and diagnostic rather than deferred until submission/hydration.
  • Storage, OpenAPI, CLI, UDS, native-tool, web, and configuration contracts are unchanged.
  • Participation validation still precedes snapshot validation, preserving the established loop_requires_live error contract.
  • The remaining provider failure observed in QA occurs after successful Run persistence and is unrelated to issue Loop validates and dry-runs but real run fails: executed definition template manifest changed #313. Live provider/child execution was deliberately not used as a substitute for the persistence boundary this issue owns.

Fixes #313

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of default values when compiling loop definitions.
    • Preserved templated inputs and modes when saving and restoring loop execution snapshots.
    • Added validation to detect missing, unexpected, or modified template and condition data.
    • Dry runs now reject invalid execution snapshots before creating a run.
  • Reliability

    • Snapshot generation now verifies that saved artifacts can be successfully reloaded.

@vercel

vercel Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
compozy-site Ready Ready Preview Aug 5, 2026 8:37pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The compiler now uses default-folded definitions consistently. Executed definition snapshots validate template and condition manifests after hydration. Dry-run performs this validation, and tests cover persisted snapshots, templated child loops, and invalid manifest changes.

Changes

Executed definition validation

Layer / File(s) Summary
Compile default-folded definitions
internal/loop/compiler.go
Compile uses the default-folded definition for linting, resolution, contracts, nodes, concurrency, and subloop compilation.
Validate snapshot manifests
internal/loop/executed_definition_manifest.go, internal/loop/executed_definition_snapshot.go, internal/loop/coordinator_snapshot_test.go
Snapshot build and load validate template and condition sources after hydration. Snapshot tests verify templated child loop inputs survive round-tripping.
Enforce validation in service flows
internal/loop/service.go, internal/loop/service_integration_test.go, internal/loop/service_test.go
DryRun validates the executed definition snapshot. Integration tests verify persisted snapshots, and service tests reject unexpected manifest keys without creating a run.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes preserving Loop template manifests during hydration, which is the primary change.
Linked Issues check ✅ Passed The changes validate snapshot round trips, preserve manifest sources, improve mismatch diagnostics, and reject invalid dry runs before Run creation.
Out of Scope Changes check ✅ Passed The reviewed compiler, snapshot, service, and test changes directly support the linked issue and stated objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-313-loop-manifest

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

100-101: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Lint the definition after compile-time defaults.

Compiler.Compile validates raw authored defaults/runtime values, lints def, then folds fan_out.batch_size, run-loop.mode, goal.session, and goal.on_exhausted. Any lint rule can reject missing default fields or warn differently from the definition used for compilation and snapshotting. Call c.linter.Lint after foldDefinitionDefaults, or make the raw-input requirement explicit.

🤖 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/compiler.go` around lines 100 - 101, Update Compiler.Compile to
invoke c.linter.Lint on the definition returned by foldDefinitionDefaults,
ensuring linting uses the same defaults-applied definition consumed for
compilation and snapshotting. Remove or relocate the earlier lint of the raw
definition while preserving existing validation behavior.
🤖 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.

Nitpick comments:
In `@internal/loop/compiler.go`:
- Around line 100-101: Update Compiler.Compile to invoke c.linter.Lint on the
definition returned by foldDefinitionDefaults, ensuring linting uses the same
defaults-applied definition consumed for compilation and snapshotting. Remove or
relocate the earlier lint of the raw definition while preserving existing
validation behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e721987e-3578-45a7-88fb-f85186464463

📥 Commits

Reviewing files that changed from the base of the PR and between 5d236a4 and f1a58b3.

⛔ Files ignored due to path filters (6)
  • docs/qa/charters/CH-loop-template-snapshot-round-trip.md is excluded by !**/*.md
  • docs/qa/journeys/J-01-arrive-and-use-run.md is excluded by !**/*.md
  • docs/qa/reports/2026-08-05-issue-313-loop-manifest.md is excluded by !**/*.md
  • docs/qa/scenarios/LP-loop-template-snapshot-round-trip.md is excluded by !**/*.md
  • packages/site/content/docs/loops/authoring.mdx is excluded by !**/*.mdx
  • skills/compozy/references/loops.md is excluded by !**/*.md
📒 Files selected for processing (8)
  • internal/loop/compiler.go
  • internal/loop/coordinator_snapshot_test.go
  • internal/loop/executed_definition_hydrate.go
  • internal/loop/executed_definition_manifest.go
  • internal/loop/executed_definition_snapshot.go
  • internal/loop/service.go
  • internal/loop/service_integration_test.go
  • internal/loop/service_test.go
💤 Files with no reviewable changes (1)
  • internal/loop/executed_definition_hydrate.go

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown

Greptile Summary

The PR makes compiled Loop definitions internally canonical before generating template manifests and validates executed-definition snapshots through the production hydration path during both snapshot construction and dry-run.

  • Folds definition defaults before template, condition, graph, watch-event, and child-Loop compilation.
  • Adds deterministic diagnostics for added, missing, and changed template or condition manifest entries.
  • Makes snapshot construction self-validate by loading the canonical snapshot before persistence.
  • Extends dry-run, focused tests, authoring guidance, and QA evidence to cover the persistence/hydration boundary.

Confidence Score: 5/5

The PR appears safe to merge, with the changed compilation, snapshot, and dry-run paths preserving strict manifest integrity without an identified regression.

Canonical defaults are now used consistently when manifests are compiled, and both dry-run and Start validate snapshots through the same deterministic loader before returning success or persisting state.

Important Files Changed

Filename Overview
internal/loop/compiler.go Canonicalizes defaults once and consistently uses the folded definition throughout compilation.
internal/loop/executed_definition_snapshot.go Adds production-loader round-trip validation during snapshot construction and precise manifest comparisons during loading.
internal/loop/executed_definition_manifest.go Implements deterministic, key-specific comparisons for template and condition manifests.
internal/loop/service.go Extends dry-run to validate the same executed-definition snapshot boundary used by real submission.
internal/loop/coordinator_snapshot_test.go Adds focused coverage for templated parent and child Loop snapshot hydration.
internal/loop/service_integration_test.go Verifies workspace-scoped persistence and hydration of default modes and child templates.
internal/loop/service_test.go Verifies dry-run rejects non-round-trippable snapshots without creating state.

Sequence Diagram

sequenceDiagram
    participant User
    participant Service as Loop Service
    participant Compiler
    participant Snapshot as Snapshot Builder
    participant Loader as Production Loader
    participant Store

    User->>Service: Dry-run or Start
    Service->>Compiler: Compile definition
    Compiler->>Compiler: Fold defaults and compile canonical form
    Compiler-->>Service: Resolved definition + manifests
    Service->>Snapshot: Build canonical snapshot
    Snapshot->>Loader: Load snapshot with computed digest
    Loader->>Compiler: Recompile persisted definition
    Loader->>Loader: Compare template and condition manifests
    Loader-->>Snapshot: Hydrated definition or precise validation error
    alt Dry-run
        Snapshot-->>Service: Validated snapshot (discard bytes)
        Service-->>User: Plan preview
    else Start
        Snapshot-->>Service: Snapshot bytes + digest
        Service->>Store: Persist Run and snapshot
        Service-->>User: Created Run
    end
Loading

Reviews (1): Last reviewed commit: "fix: preserve Loop template manifests ac..." | Re-trigger Greptile

This branch was successfully deployed

1 active deployment
Preview — f1a58b33 Deployed Aug 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Loop validates and dry-runs but real run fails: executed definition template manifest changed

1 participant