Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(playbook): persist compiled manifest inline in writePlaybookXml so structural edits don't spuriously revert

merged#2463CedarCopilot

CedarCopilot wants to merge 0 commits into staging from fix/playbook-xml-persist-manifest-before-verify

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 12, 2026, 6:43 PM
  2. Merged
    Aug 12, 2026, 7:44 PM
  3. Live on prod
    Aug 12, 2026, 7:44 PM
  4. Observed 30 hours
    Aug 12, 2026, 7:44 PM
  5. Pipelines steady after this deploy
    Aug 12, 2026, 7:44 PM
  6. Unobserved

    Live on prod, no production signal yet

    Aug 14, 2026, 1:36 AM

Behaviors Libra is checking

Playbook patch writes apply oldString-to-newString against live content while carrying fresh compiled metadata, preserving unrelated concurrent edits instead of replacing the document from a stale snapshot.Inconclusivelow confidence

OTEL fallback found no prod spans matching writePlaybookXml patch mode since deploy.

prod, checked Aug 14, 2026, 12:36 AM
Playbook structural edits persist the freshly compiled playbook and manifest with the content so verification does not spuriously revert valid writes when Yjs synchronization is skipped.Inconclusivelow confidence

OTEL fallback found no prod spans matching writePlaybookXml since deploy.

prod, checked Aug 14, 2026, 12:37 AM
When post-write playbook verification fails, rollback restores both the prior XML content and its prior metadata so rejected compiled manifests are not left attached to reverted content.Inconclusivelow confidence

OTEL fallback found no prod spans matching writePlaybookXml verification rollback since deploy.

prod, checked Aug 14, 2026, 1:36 AM

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.

Problem

Agent / MCP / CLI-driven structural edits to a user PLAYBOOK.md , adding or removing a <trigger> block or a <ref> , were silently reverted with STALE_COMPILED_PLAYBOOK, even though the edit was valid. Prose-only edits (cross-cutting text, stage bodies) and all resource/subagent edits worked fine.

Found while trying to unregister a user's inbound-email-notifier: every attempt to remove its <trigger type="email"> block reverted, while editing subagent bodies succeeded.

Root cause

writePlaybookXml wrote the new XML but left compiled_playbook metadata to be regenerated by the Yjs-flush compile hook (playbookCompileHook, run inside writeFileAsYjs). That hook is skipped whenever the caller passes skipYjsSync (documents/index.ts , the block is gated on !params.skipYjsSync), and the live write-document tool always sets it because it needs the yjsResult itself.

So the sequence for a tool-driven edit was:

  1. write new XML (skipYjsSync → compile hook does not run) → stored compiled_playbook.manifest stays one edit stale
  2. verifyPlaybookcheckCompiledFreshness recompiles the new content and compares to the stale stored manifest → mismatch → STALE_COMPILED_PLAYBOOK
  3. revert

Only structural edits tripped it, because only they change the manifest (eventTypes, fieldWatchers, cronSchedules, beforeMeetingConfigs, hasGlobalBlock). Prose edits leave the manifest identical, so stored == recompile and verify passed , exactly the asymmetry observed. Org playbooks were immune because writeOrgPlaybookXml already compiles + persists the manifest inline.

Verified empirically against the affected playbook: a fresh recompile of the unchanged XML matched the stored manifest exactly (EQUAL? true) , i.e. no data drift; the failure was self-inflicted by the write path on the structural change.

Fix

Capture compiled + manifest from the pre-write compile and write them into metadata (compiled_playbook / playbook_manifest) in the same upsert, before verify , mirroring what writeOrgPlaybookXml already does. A patch is resolved into full prospective XML and written as an upsert so the metadata can ride along. Existing metadata is preserved (merge, not replace).

This makes freshness independent of whether the Yjs hook runs, so skipYjsSync no longer causes a false-positive revert. When the hook does run (non-tool callers), it recompiles to the same value , idempotent.

Tests

pnpm --filter @zero/server exec vitest run src/services/playbook/__tests__/guarded-write.test.ts24 passed (22 existing + 2 new). Server typecheck clean for the changed file.

New regression tests assert the freshly compiled manifest is persisted in the same write for both the skipYjsSync: true upsert path and the structural-patch path.

🤖 Generated with Claude Code

Greptile Summary

This PR persists freshly compiled playbook metadata in the same write as structural content changes so verification no longer compares new XML with a stale manifest.

  • Converts patch writes into full-content upserts carrying compiled_playbook and playbook_manifest.
  • Preserves existing metadata through a read-and-spread merge.
  • Adds regression coverage for skipYjsSync and structural patch writes.

Confidence Score: 3/5

The PR should not merge until patch writes preserve concurrent edits and failed verification restores metadata consistent with the reverted content.

Converting patches to unconditional full-document upserts creates a lost-update path, while persisting new compiled metadata without restoring the old metadata during rollback leaves reverted documents stale.

Files Needing Attention: apps/server/src/services/playbook/guarded-write.ts

Important Files Changed

FilenameOverview
apps/server/src/services/playbook/guarded-write.tsP
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/services/playbook/__tests__/guarded-write.test.tsno production surface mapped
  • apps/server/src/services/playbook/guarded-write.tsno production surface mapped