Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(tasks): detect scheduled runs that record no outcome; let the run decide

merged#2620CedarCopilot

CedarCopilot wants to merge 3 commits into staging from fix/scheduled-task-unrecorded-outcome

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 29, 2026, 4:11 PM
  2. Aug 30, 2026, 3:30 PM
  3. Merged
    Aug 30, 2026, 5:01 PM
  4. Live on prod
    Aug 30, 2026, 5:01 PM
  5. Observed 2 hours
    Aug 30, 2026, 5:01 PM
  6. Unobserved

    Live on prod, no production signal yet

    Aug 30, 2026, 5:01 PM
  7. Pipelines steady after this deploy
    Aug 30, 2026, 5:01 PM

Behaviors Libra is checking

When a scheduled-task run cannot complete work but the task is still needed, it must leave the task todo and record the blocking reason in its notes via update-task.Not checked
prod
Scheduled-task executions must explicitly record whether they acted, determined the task obsolete, or remain blocked before finishing.Not checked
prod
When a scheduled-task run determines that its work is obsolete, it must call cancel-task so the task is recorded as agent_deleted rather than done.Not checked
prod
Failures while checking for an unresolved scheduled task must be logged and treated as a non-match so promotion continues instead of throwing.Not checked
prod
Promotion must not cancel or otherwise mutate a task merely because its scheduled run completed without an artifact, leaving the run's agent to decide whether the task is obsolete or still needed.Not checked
prod
Promotion must detect completed scheduled runs whose task is still todo and has no produced artifact, then emit a structured diagnostic containing the task, user, run, and summary.Not checked
prod

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

Reworked after review. The first version cancelled these tasks. That was wrong, and the reason is the point of the PR , see below.

What's wrong

executeTask claims a task and runs the orchestrator. The run finishes, and nothing writes the outcome to the task. The row stays todo with executionRunId set, and on the next promotion tick promoteScheduledTasks sees a completed execution and deletes the KV entry. The task is then permanently overdue, open, and unscheduled , it will never run again, and no programmatic path will ever close it.

377 tasks are in that state, accruing at roughly 95 a month.

Why this doesn't cancel them

The first version did. It treated "run completed + no artifact + still todo" as meaning the work stopped being necessary. It doesn't , the same observable state covers three different situations, and only one is obsolete:

what actually happenedstill needed?
"No action needed as collaborator"no , cancelling is right
couldn't proceed: missing contact, ambiguous data, blockedyes
errored partway and still closed outyes

Auto-cancelling deletes the user's commitment in two cases out of three, silently. That is the same class of failure as the direction-blind close fixed in #2618 , something deciding a commitment is finished when it isn't.

The distinguishing information exists only inside the run. So the run makes the call.

What changed

1. executeTask instructs the executor to record its outcome. Three branches, stated explicitly:

  • did the work โ†’ nothing further, the artifact is the record
  • obsolete โ†’ cancel-task, which writes agent_deleted. Never status: 'done', which claims the work happened
  • still needed but blocked โ†’ leave it open and say why in the notes

2. isScheduledTaskUnresolved detects and logs. It never writes. Same predicate as before, read-only, hooked into the same place , promoteScheduledTasks' completed-execution branch, which already proves the rest of the precondition (task exists, agent execution on, status = 'todo', dueDate unchanged) and is the last moment anything looks at the task.

['cedar-prod']
| where message == "[promoteScheduledTasks] Completed run recorded no outcome"
| summarize count() by bin(_time, 1d)

Falling means the instruction lands. Flat or rising is the case for something stronger , made with evidence rather than assumed up front.

3. Dropped the task-notes write, which only existed to explain a cancellation that no longer happens.

Verified

  • tsc -b on @zero/server , clean
  • pnpm deps:check , no violations (1577 modules, 8224 dependencies)
  • oxlint , clean; the single warning is in deal-audit.ts, outside this diff
  • All four vitest shards. organizations.test.ts fails in shard 2 and is pre-existing , reproduces on a clean origin/staging worktree
  • scheduled-task-unresolved.test.ts , 4 tests, including an explicit assertion that the detector performs no write even when it detects the state

Limitation

Forward-looking. The ~377 already-stranded tasks had their KV entries deleted when their runs completed, so promotion will never see them again. Draining that backlog is separate, and now clearly needs the same per-task judgement rather than a sweep.

Also includes the branch-C design doc, with ยง2 corrected to describe this approach and record why the cancelling one was rejected.

๐Ÿค– Generated with Claude Code

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/task-scheduling/execution.tsno production surface mapped
  • apps/server/src/services/task-scheduling/promotion.tsno production surface mapped
  • apps/server/src/services/user-tasks/__tests__/scheduled-task-unresolved.test.tsno production surface mapped
  • apps/server/src/services/user-tasks/tasks.tsno production surface mapped
  • docs/design/task-completion-keys-and-scheduled-execution.mdno production surface mapped