CedarCopilot wants to merge 1 commit into staging from fix/direct-fire-participant-guard
Live on prod, no production signal yet
OTEL fallback found no prod spans matching runPlaybookSectionExecution since deploy.
OTEL fallback found no prod spans matching runPlaybookSectionExecution since deploy.
OTEL fallback found no prod spans matching runPlaybookSectionExecution since deploy.
OTEL fallback found no prod spans matching [runPlaybookSectionExecution] direct-fire resolved zero refs since deploy.
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.
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.
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.
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:
Unresolvable ownership fails open. A duplicate brief is recoverable by the reader; a missing one is not.
Only two org playbooks in production carry a trigger block on a conversation-scoped direct-fire path:
| Org | Block | Effect |
|---|---|---|
| SimCare | org before-meeting → meeting-prep | stops the duplicate briefs |
| Mastra | org before-meeting → meeting-prep, org field-change ×2 → deal-channel-provisioner | prep 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.
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
Libra has not measured any production surfaces for this change yet.