Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(aop): allow cloning a no-op AOP source with no playbook tree

merged#2891CedarCopilot

CedarCopilot wants to merge 2 commits into staging from fix/aop-clone-noop-source

What Libra verified

  • A no-op AOP clones into another document-less no-op AOP

    Libra ran this behavior against the change and confirmed the check detects when it breaks.

    Technical evidence
    • cloning an existing no-op AOP with no playbook documents succeeds with zero documents and marks both created AOP rows as no-op

    Changed code: create-aop.test.ts.

  • Cloning preserves no-op execution semantics in both AOP representations

    Libra ran this behavior against the change and confirmed the check detects when it breaks.

    Technical evidence
    • cloning a no-op source preserves isNoOp=true on both the new user-level and organization-level rows

    Changed code: create-aop.ts.

Live on prod, watching, 2 days leftTimeline and evidence
  1. Opened
    Sep 22, 2026, 6:33 PM
  2. Sep 22, 2026, 7:56 PM
  3. Merged
    Sep 22, 2026, 8:16 PM
  4. Live on prod
    Sep 22, 2026, 8:16 PM
  5. Observed 0 hours
    Sep 22, 2026, 8:16 PM
  6. Watching

    Live on prod, watching, 2 days left

    Sep 22, 2026, 8:16 PM
  7. Pipelines steady after this deploy
    Sep 22, 2026, 8:16 PM

Behaviors Libra is checking

Cloning a genuine no-op AOP with no playbook documents succeeds and creates an equally document-less AOP.Not checked
prod
A clone inherits the source AOP's isNoOp status in both the user-level and organization-level AOP records, so cloned no-op buckets remain excluded from execution.Not checked
prod
Cloning a non-no-op AOP with an absent or empty playbook tree continues to fail and rolls back both newly inserted AOP records.Not checked
prod

Libra has verdicts on 0 of 3 tracked behaviors on prod; 3 are still being checked. Libra checks hourly for 3 days after each deploy.

Summary

  • cloneAop() required the clone source to have a real playbook document tree, but every genuine no-op AOP (Spam, Recruiting, Vendor notification, ...) is seeded with isNoOp: true and deliberately zero documents , there was no valid source to clone a new pure-classification bucket (e.g. a "Personal" category) from at all.
  • cloneAop() also hardcoded isNoOp: false on both DB inserts regardless of the source, so even a successful clone of a no-op AOP would silently stop being a no-op.
  • Found via a real customer chat transcript: a user asked Cedar's assistant to add a "Personal" category, and the agent's three create-aop attempts (Recruiting → "no playbook document tree to clone", then Suppliers and Deals → playbook validation errors from cloning a live automation playbook) all failed.

Changes

  • cloneAopDocuments now returns an empty result instead of throwing when the source has zero documents and is itself a no-op AOP. A non-no-op source with zero documents is still treated as corrupted state and rejected (existing behavior preserved, now covered by its own test).
  • cloneAop now inherits isNoOp from the source AOP instead of hardcoding false (mirrors how scope already defaults from the source).
  • Updated the aop-write tool's cloneFromAopId description so the agent clones from an existing no-op AOP for a pure classification category, instead of trying to clone and strip down a live playbook (which is what led it down the failing Suppliers/Deals path in the real transcript).
  • Updated create-aop.test.ts: replaced the test asserting the old (buggy) throw-on-no-op-source behavior with one asserting the new success path, and added a companion test proving the corrupted-state guard still holds for a non-no-op source with no documents.

Test plan

  • pnpm --filter @zero/server run types , clean
  • pnpm --filter @zero/server exec vitest run src/services/aop/__tests__/create-aop.test.ts , 19/19 passing
  • Verified live: manually invoked the fixed cloneAop()/reassignConversationAop() functions directly against prod (via a one-off script, since this fix isn't deployed yet) to create the real "Personal" AOP for the customer who hit this and reassign their conversation to it , confirmed isNoOp: true on the new AOP row and aopName: "Personal" on the reassigned conversation.

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the intended no-op cloning path is narrowly gated and the existing corrupted-state rejection remains intact.

Summary

This PR enables cloning a genuine no-op AOP whose deliberately empty document tree previously caused cloning to fail.

  • Inherits isNoOp on both newly created AOP rows.
  • Allows an empty source tree only when the source is marked no-op.
  • Preserves rejection and cleanup for non-no-op sources with missing document trees.
  • Updates agent guidance and tests for the new behavior.

Diagram

rendering diagram…

Revie

Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • apps/server/src/mastra/tools/config/aop-write-tool.tsno production surface mapped
  • apps/server/src/services/aop/__tests__/create-aop.test.tsno production surface mapped
  • apps/server/src/services/aop/create-aop.tsno production surface mapped