Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

revert(cron): org-level cron fires once again, not once per linked person

merged#2793CedarCopilot

CedarCopilot wants to merge 2 commits into staging from fix/org-cron-single-fire

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 15, 2026, 8:27 AM
  2. Sep 15, 2026, 8:42 AM
  3. Merged
    Sep 15, 2026, 8:52 AM
  4. Live on prod
    Sep 15, 2026, 8:52 AM
  5. Observed 3 days
    Sep 15, 2026, 8:52 AM
  6. Pipelines steady after this deploy
    Sep 15, 2026, 8:52 AM
  7. Unobserved

    Live on prod, no production signal yet

    Sep 18, 2026, 8:52 AM

Behaviors Libra is checking

When eligible representative candidates tie on connection and expiry health, org cron selects the lowest stable AOP identifier so the representative does not churn between ticks.Brokenlow confidence

Verification reached its 130s outer budget before the verifier returned. Libra is keeping this intent verifying until the next check.

prod, checked Sep 18, 2026, 8:51 AM
A person linked through multiple AOPs contributes only one eligible representative candidate for an org cron tick.Inconclusivelow confidence

Verification reached its 130s outer budget before the verifier returned. Libra is keeping this intent verifying until the next check.

prod, checked Sep 17, 2026, 7:59 PM
When several eligible people are available, org cron prefers a representative with a Google `direct_oauth` connection containing both access and refresh tokens, using the freshest expiry among usable connections and falling back to an unusable candidate only when necessary.Inconclusivelow confidence

Verification reached its 130s outer budget before the verifier returned. Libra is keeping this intent verifying until the next check.

prod, checked Sep 18, 2026, 12:51 AM
An org cron can select only a linked person whose `agentExecutionEnabled` is not explicitly false, while unset or unrelated settings remain enabled by default, and no cron is scheduled when nobody is eligible.Inconclusivelow confidence

Verification reached its budget before a verdict.

prod, checked Sep 18, 2026, 12:53 AM
An eligible linked person with no Google connection row remains in the representative pool as an unusable, last-ranked candidate instead of being silently discarded.Inconclusivelow confidence

Unable to complete the telemetry verification because the available evidence confirms representative-selection logs but does not establish the required cron-config count, latency, distinct-user metrics, or tied failure baseline. Verdict: inconclusive.

prod, checked Sep 18, 2026, 8:52 AM
When exactly one eligible person is linked, representative selection returns that person immediately without querying Google or Slack connection tables.Not checked
prod

Libra has verdicts on 0 of 8 tracked behaviors on prod; 8 are still being checked. Libra has 1 low-confidence broken verdict it is still confirming. Libra checks hourly for 3 days after each deploy.

Summary

Reverts the load-bearing part of feat: an org cron fires for every linked person, not one of them (design: sharing phase 2) (b799f7c2f, 2026-09-09) that changed getPlaybookCronConfigs to schedule one execution per every person linked to an org playbook's AOP, instead of picking one representative.

Why this is a revert, not just a patch: the stated justification for the original change , a coaching agent shared with six reps produces one run, and five people silently get nothing , doesn't hold up. coach-weekly's own header comment already explained, before the change, that it self-fans-out over every rep from a shared KB folder in one run because it was written knowing the org cron only fires once. It was never depending on the dispatcher to iterate reps. Firing per person instead didn't rescue five starved reps; it made coach-weekly redundantly re-run and re-write every rep's overview once per linked person.

What firing-per-person actually broke: every org-level agent that intentionally produces one shared aggregate output. On 2026-09-14, weekly-competitor-report (Aspireiq) and competitive-blocker-report (Pirros) , both authored to "fire once for the whole org, not once per rep" , each posted one duplicate copy of that week's report per linked rep: 6 duplicate posts into #cedar-competitors, 3 into #barriers-to-buy. Confirmed against production data (agent_executions rows, one per linked rep, all with the same agent_id, all at the org's scheduled tick time).

Kept from the reverted change (this part was a real, separate fix and stays):

  • The kill switch is read per person, before a representative is even chosen , a rep who turned agentExecutionEnabled off is never eligible to be picked.
  • A person is counted once even when they link multiple AOPs to the org AOP.

Restored:

  • pickOrgCronRepresentative, OrgCronCandidate, OrgCronConnectionRow, buildOrgCronCandidates, rankOrgCronCandidates, markSlackConnectedCandidates , connection + Slack health ranking, now layered on top of the kill-switch-filtered eligible pool (orgCronParticipants) rather than replacing it. getPlaybookCronConfigs's org branch schedules exactly one config per schedule again.
  • Test coverage for both the eligibility gate (org-cron-eligibility.test.ts, migrated/renamed from the deleted per-user-runs.test.ts) and the health-ranking pick (org-cron-representative.test.ts, restored from before the deletion, plus a new test proving a kill-switch-excluded person never enters the ranking pool at all).
  • coach-weekly.ts's header comment, restored to describe the single-fire model it was actually written for.
  • docs/sharing.md's Phase 2 section, amended in place (not rewritten) to record the revert, what's kept, and what it reopens.

Explicitly not addressed: docs/sharing.md's later Phase 4 ("Rollups… REMOVED") and Phase 6 ("Retire the workaround") both reasoned from the now-reverted "a run belongs to one person" premise. Phase 6 was never built ([ ], unshipped), so nothing broke, but neither should be treated as settled anymore , flagged inline in the doc for whoever picks either back up.

Test plan

  • pnpm --filter @zero/server run types , clean
  • pnpm --filter @zero/server exec vitest run src/services/playbook/__tests__/org-cron-eligibility.test.ts src/services/playbook/__tests__/org-cron-representative.test.ts src/services/playbook/__tests__/manifest-hook.test.ts src/services/playbook/__tests__/resolve-playbook-context.test.ts , 67/67 passing
  • eslint on every touched file , clean
  • Confirmed via git grep that no other code (UI, other services) calls orgCronParticipants/selectCronParticipants/getPlaybookCronConfigs in a way that depended on the per-person fan-out , the only caller is getPlaybookCronConfigs itself
  • After merge: confirm next Monday's org-level cron ticks (Aspireiq weekly-competitor-report, Pirros competitive-blocker-report) pro
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/docs/sharing.mdno production surface mapped
  • apps/server/src/services/agents/__tests__/per-user-runs.test.tsno production surface mapped
  • apps/server/src/services/playbook/__tests__/org-cron-eligibility.test.tsno production surface mapped
  • apps/server/src/services/playbook/__tests__/org-cron-representative.test.tsno production surface mapped
  • apps/server/src/services/playbook/agent-defaults/coach-weekly.tsno production surface mapped
  • apps/server/src/services/playbook/manifest-utils.tsno production surface mapped