Skip to content

[Bug]: scheduled tasks: run recorded as succeeded when the run fails; missed fixed-time slots silently skipped #30844

Closed

Description

@Giovarco

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Steps to reproduce

My use case, because I think it explains why this matters: I run an orchestrator-style workflow in a project. A top-level chat delegates to large numbers of subagents across several models, and the orchestrator skill keeps bookkeeping in .orchestrator/: per-model notes with facts about each subagent run (task type, outcome, rework, failures). To make sure that daily bookkeeping actually happens, I created a scheduled T3 Code task: every morning at 08:00 it reviews yesterday's git activity and subagent runs and updates the model notes. The prompt is self-contained and the task runs on a paid model (mimo-v2.6-flash) on an unattended machine. Unattended is the point — the status line is the only signal that it worked.

Reproduction (T3 Code server v0.1.5 / opencode 2.2.5):

  1. Create a scheduled task with schedule: {type:"fixed_time", timeOfDay:"08:00"}, bound to a runner thread, running a paid model whose plan quota can be exhausted.
  2. Reach the provider's usage limit (e.g. weekly quota) and let the schedule fire.
  3. For the second bug: on several days, have the machine off at the fixed-time slot.

Expected behavior

  • lastRunStatus / lastRunError should reflect the terminal state of the run in the bound thread (failed, interrupted, cancelled), not just "the prompt was posted and a run was created".
  • The user should be notified (or the thread flagged/readable as failed) when an automated scheduled run fails, especially when the agent produced neither a report nor file changes.
  • Missed fixed-time slots should at least be visible: either catch up on boot (optionally with a max age, like a catchUpWindow), or surface "skipped N missed slots" in the task status.

Actual behavior

  • The thread run failed in ~1 second with the provider error (OpenCode Go weekly usage limit reached), but list_scheduled_tasks showed lastRunStatus: "succeeded" and last_run_error: null.
  • Independent confirmation: the scheduler's own record (last_run_status: "succeeded", last_run_error: null) contradicted the bound thread's run record for that minute, which reads status: "failed" with the provider error.
  • On the second bug: after several days with the machine off at 08:00, no run happened for any missed day; nextRunAt had already advanced to the following day and lastRunStatus kept the last OK value from an earlier manual run.
  • Net effect: the task looked green across a whole week while the automation's target files had their last update a week earlier and zero failure notifications arrived. Unattended automations are the one place where the status line is the only signal, so a silent no-op agent looks identical to a silent-but-healthy one; the breakage was only detectable by manually diffing the output files.

Impact

Major degradation or frequent failure

Version or commit

No response

Environment

  • opencode version: 2.2.5 (T3 Code server reports serverVersion 0.1.5) - OS: Linux x64, Ubuntu, kernel 7.0.0-42-generic - Shell: /bin/bash - Scheduled task runs headless / server-side (no terminal involved) - Active plugins: none

Logs or stack traces

Screenshots, recordings, or supporting files

No response

Workaround

What keeps it alive today (unfixed semantics, reduced blast radius):

  1. Replaced fixed_time with interval scheduling (every 20h): a missed slot fires on the next boot if the machine was off, so downtime no longer silently skips days.
  2. Prompt made self-healing: each run backfills every missing day since the newest dated entry (not just yesterday), and must grep-verify its own writes before reporting. Verified this in practice: after a week of missed days, the first successful run backfilled all seven days correctly.
  3. For the quota variant specifically: Extra Usage on the OpenCode Go plan removes the recurring weekly failure.

Possible fixes in the product: write the run's terminal status into lastRunStatus (a posted prompt should map to running, not succeeded); notify the user on failed scheduled runs; optionally expose the last executed runId next to the status for cross-checking; consider an optional catch-up window for missed fixed-time slots.

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    needs-triageIssue needs maintainer review and initial categorization.
    on Nov 4, 2026
  2. chrisfonte commented on Nov 5, 2026

    @chrisfonte

    I found a head-of-line path that can explain a missed fixed-time task even while the server process stays up. At 0.1.6-nightly.20261030.3940 (ae521d1), Scheduler.ts uses one permit per source and ScheduledTaskService.runDueTasks awaits due task dispatches sequentially, without a dispatch timeout. One hung launch/send therefore delays the remaining due tasks. Tasks already in that due batch use tick-start time and run late; tasks becoming due during the hang can be skipped on the next tick. When dispatch resumes, a fixed_time task more than ten minutes late is skipped and advanced to its next occurrence; its prompt and self-disable never run.

    In one deployment, a bound dispatch remained outstanding through a filesystem outage. Another fixed-time task became due within that span and subsequently showed the following week's slot with no corresponding scheduled message. The exact blocking call and skip log were unavailable after trace rotation, so this particular skip explanation is inferred from source and the observed timeline, not a captured skip-log proof. This is separate from an on-time queued message whose provider turn starts much later.

    It may help to expose dispatch versus provider-start/completion separately, make missed-slot skips visible in task history, and bound dispatch waits so one blocked task cannot delay unrelated due tasks. Native once in v0.1.7 runs late after downtime, but does not remove the sequential-dispatch concern.

    Source:

    yield* Effect.forEach(
    due,
    ({ task, dueAt }) =>
    Effect.suspend(() =>
    isMissedFixedTimeRun(task.schedule, dueAt, now)
    ? rescheduleMissedRun(task, now)
    : runTask(task, "scheduled"),
    ).pipe(
    Effect.catch((cause) =>
    Effect.logWarning("Scheduled task run failed", { taskId: task.id, cause }),
    ),
    ),
    { concurrency: 1, discard: true },
    and
    const MISSED_FIXED_TIME_GRACE_MS = 10 * MINUTE_MS;
    export function isMissedFixedTimeRun(
    schedule: ScheduledTaskSchedule,
    dueAt: DateTime.DateTime,
    now: DateTime.DateTime,
    ): boolean {
    if (schedule.type !== "fixed_time") return false;
    return DateTime.toEpochMillis(now) - DateTime.toEpochMillis(dueAt) > MISSED_FIXED_TIME_GRACE_MS;
    }
    . These are code/timeline findings, not a new controlled runtime reproduction.

  3. added 3 commits that reference this issue on Nov 12, 2026
    922020b
    41e9ed6
    7ffb54b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.needs-triageIssue needs maintainer review and initial categorization.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions