Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(playbook): direct-fire triggers must respect conversation ownership

merged#2580CedarCopilot

CedarCopilot wants to merge 1 commit into staging from fix/direct-fire-participant-guard

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 25, 2026, 5:20 PM
  2. Aug 25, 2026, 5:26 PM
  3. Merged
    Aug 25, 2026, 5:38 PM
  4. Live on prod
    Aug 25, 2026, 5:38 PM
  5. Observed 37 hours
    Aug 25, 2026, 5:38 PM
  6. Pipelines steady after this deploy
    Aug 25, 2026, 5:38 PM
  7. Unobserved

    Live on prod, no production signal yet

    Aug 27, 2026, 6:11 AM

Behaviors Libra is checking

Conversation-scoped authored direct-fire playbook triggers now resolve organization-level blocks only in the conversation owner's context, preventing duplicate automations from being dispatched for other attendees while preserving their own user-level refs.Inconclusivelow confidence

OTEL fallback found no prod spans matching runPlaybookSectionExecution since deploy.

prod, checked Aug 27, 2026, 3:11 AM
When conversation ownership cannot be read or the conversation row is missing, direct-fire execution continues with organization-level blocks rather than silently cancelling the automation.Inconclusivelow confidence

OTEL fallback found no prod spans matching runPlaybookSectionExecution since deploy.

prod, checked Aug 27, 2026, 4:11 AM
A participant whose direct-fire context contains neither user-level refs nor free text now exits without dispatching the legacy hash-fallback automation.Inconclusivelow confidence

OTEL fallback found no prod spans matching runPlaybookSectionExecution since deploy.

prod, checked Aug 27, 2026, 5:11 AM
Participant direct-fire executions that correctly resolve no organization-level refs no longer emit misleading zero-ref warning logs.Inconclusivelow confidence

OTEL fallback found no prod spans matching [runPlaybookSectionExecution] direct-fire resolved zero refs since deploy.

prod, checked Aug 27, 2026, 6:11 AM

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

The defect

runPlaybookSectionExecution passed isParticipantExecution: false to resolvePlaybookContext unconditionally, so every direct-fire run declared itself the conversation owner.

Both neighbouring layers already implement the intended contract:

  • resolve-playbook-context.ts:235,501 excludes org trigger blocks for participants.
  • calendar-events.ts:401 refuses to even schedule an org-level before-meeting config for one, commenting that org-level subagents "must run once, in the owner's context".

The executor re-resolves context from scratch and asserted its way past both, so an org-level brief ran once per attendee and posted once per attendee into a single shared channel.

Evidence

SimCare, meeting 29vomku9b7h06gas6rc88tgb52:

<email>      (owner)  …:playbook:a7c6f8bb:global:60                     <- org aop
<email> (editor) …:playbook:c6ba6525:global:60:user-2JtcrD…        <- his own aop

The scheduler scoped Vrishank correctly, right down to the :user- dedupe suffix. The executor then ran the org agent for him anyway, 90 seconds after the owner's. Twelve runs across six meetings in three days, every one doubled, both copies into the same shared channels.

Event-type triggers were unaffected: they route through the orchestrator, which carries its own participant gate in the prompt. Direct-fire skips the orchestrator entirely and so had no gate at all.

The change

Derived inside the executor rather than added as a caller parameter , the defect is a layer that forgot to pass it, and no direct-fire caller can supply a value this cannot compute. All six production call sites were traced; four pass either no conversationId or the owner's own id by construction, so only before-meeting and field-change change behaviour.

Scoped to the authored trigger types. conversation_review is excluded deliberately: it is a dispatch mode rather than a playbook-authored trigger, and whether a non-owner review should still resolve org refs is a separate question with no evidence behind it. Widening later is one line.

Two consequences handled:

  • The zero-refs warning is exempted for participants, where resolving nothing is the designed outcome rather than the misconfiguration that alert exists to surface.
  • A participant that resolves nothing returns early instead of falling through to the legacy hash fallback, which would otherwise fire a no-instruction automation for exactly the attendees this change exists to keep quiet.

Unresolvable ownership fails open. A duplicate brief is recoverable by the reader; a missing one is not.

Blast radius

Only two org playbooks in production carry a trigger block on a conversation-scoped direct-fire path:

OrgBlockEffect
SimCareorg before-meetingmeeting-prepstops the duplicate briefs
Mastraorg before-meetingmeeting-prep, org field-change ×2 → deal-channel-provisionerprep already deduped by a prompt guard, this makes it structural; the provisioner is idempotent by its own check

Pirros has an org cron block, but cron carries no conversation, so ownership can never be evaluated. Every other org's before-meeting and field-change triggers are user-level, where the guard does not apply.

Testing

7 new tests in direct-fire-participant-guard.test.ts. Two fail when the derivation is reverted to the pre-fix false , the two that pin the defect. The rest pin the surrounding contract: cron with no conversation, review exclusion, fail-open, and that a participant still runs their own user-level refs.

Verified: vitest run src/services/playbook/ → 33 files / 685 tests pass · depcruise src → no violations · tsc --noEmit → only the pre-existing src/mastra/index.ts TS7056 error, on a file this branch does not touch · eslint clean on both changed files.

🤖 Generated with Claude Code

Greptile Summary

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__/direct-fire-participant-guard.test.tsno production surface mapped
  • apps/server/src/services/playbook/playbook-execution-triggers.tsno production surface mapped