Skip to content

feat: add automation scheduler timers - #984

Merged
Astro-Han merged 21 commits into
devfrom
codex/i950-automation-pr3-scheduler
May 30, 2026
Merged

feat: add automation scheduler timers#984
Astro-Han merged 21 commits into
devfrom
codex/i950-automation-pr3-scheduler

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the PR3 backend scheduler slice for issue #950: a per-instance in-memory automation scheduler service for one-shot and interval-after-completion definitions, scheduled unattended execution, and no-overlap skip recording.

The scheduler now owns one interruptible Effect-backed task per scheduled automation, tracks the unattended runs it starts so shutdown can cancel them, and applies recurring stop rules that are safe for this in-memory slice. Native timers are contained behind the scheduler clock/sleep adapter instead of being the service architecture.

Why

PR1 froze the automation contract and PR2 made manual runNow execute safely. This PR adds background firing with the smallest useful runtime behavior while keeping the scheduler shape aligned with the broader supervised backend service design. Persistence, cron, durable locks, worktree placement, frontend, and tool-unhide remain separate later slices.

Related Issue

Closes part of #950: PR3 scheduler timers + ownership + unattended policy.

Human Review Status

Pending

Review Focus

Please focus on the scheduler ownership boundary, interruptible wait/run lifecycle, long-delay waiting, completed one-shot rescheduling, recurring no-overlap behavior, recurring stop=count handling, condition-stop non-scheduling until an evaluator exists, and recurring interval anchoring after either scheduled or manual run completion.

Risk Notes

The scheduler is intentionally in-memory and per app instance for this slice. It is now modeled as a supervised scheduler service with cancelable per-definition tasks and cancelable scheduler-owned unattended runs, while the actual waiting remains process-local. Durable cross-window locking, persistence, cron, missed-run scan, restart reconciliation, and durable nextFireAt/nextFires state are deferred to later slices per the frozen plan.

For this PR, nextFireAt / nextFires remain contract fields and are not a source of truth for the in-memory scheduler queue. The real scheduler ownership lives inside the per-instance scheduler.

Condition-based recurring stops are accepted by the frozen contract, but PR3 does not include a condition evaluator. To avoid turning an unevaluated condition into an infinite unattended loop, condition-stop definitions are not scheduled by this in-memory scheduler until that evaluator slice exists.

Skipped checklist items:

  • Visible UI check: no visible UI or copy changed.
  • Platform/packaging check: no platform, packaging, updater, signing, shell, or native path behavior changed.
  • Docs/release/dependency check: no docs, release notes, dependencies, credentials, generated content, or local file behavior changed.

How To Verify

Whitespace check: git diff --check -> passed
Automation scheduler regression: bun test test/server/automation-scheduler.test.ts --timeout 30000 -> 26 passed
Automation regression bundle: bun test test/server/automation-runner.test.ts test/server/automation-scheduler.test.ts test/server/automation-routes.test.ts test/server/automation-event-fixtures.test.ts test/server/event-replay.test.ts test/tool/automate.test.ts --timeout 30000 -> 104 passed
Typecheck: bun --cwd packages/opencode typecheck -> passed

Screenshots or Recordings

Not applicable. No visible UI changes.

Checklist

How to use this checklist:

  • Tick a box by replacing [ ] with [x]. Do not edit, add, or remove items.
  • The bot-applied label items can only be honestly ticked AFTER the PR is opened and the labeler / priority-triage bots have run — return to the PR description and tick them then.
  • Most items are required. The few that are conditional are explicitly marked (conditional); for those, leave unticked if they truly do not apply and explain why in Risk Notes. All other items must be ticked before requesting human review.
  • Type label — this PR carries exactly one of bug, enhancement, task, documentation. Type labels are author-added; the labeler bot does NOT assign them. Add the label in the GitHub UI, then tick this.
  • Routing labels — this PR carries at least one of app, ui, platform, harness, ci. The labeler bot assigns these on PR open based on changed paths. Confirm the bot's choice (or override if wrong), then tick this.
  • Priority label — this PR carries exactly one of P0, P1, P2, P3. The priority-triage bot suggests one on PR open. Confirm or override, then tick this.
  • Human Review Status above is set to Pending, Approved by @<reviewer>, or Not required: <reason> (default is Pending; "not required" is restricted to bot-authored low-risk PRs).
  • I linked the related issue, or stated in Summary why there is no issue.
  • I described the review focus and any meaningful risks.
  • I replaced the example block in How To Verify with the real verification steps and the key result for each.
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope.
  • (conditional) I manually checked visible UI or copy changes when needed, with screenshots or recordings. Leave unticked only if no visible UI or copy changed.
  • (conditional) I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes. Leave unticked only if no platform/packaging surface was touched.
  • (conditional) I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant. Leave unticked only if none of those surfaces was touched.
  • I reviewed the final diff for unrelated changes and suspicious dependency changes.
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English.

Summary by CodeRabbit

  • New Features

    • Added a scheduler for automations: recurring and one-shot scheduling, missed-schedule handling, and lazy startup.
    • New run-query helpers and ability to record stopped runs with custom timestamps.
  • Bug Fixes

    • More robust active-run tracking and safer cleanup to avoid stopping newer executions.
    • Better coordination between manual and scheduled runs to prevent overlap.
  • Tests

    • Comprehensive scheduler and route tests covering scheduling, cancellation, and edge cases.

Review Change Stack

@Astro-Han Astro-Han added enhancement New feature or request P1 High priority harness Model harness, prompts, tool descriptions, and session mechanics labels May 29, 2026
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@Astro-Han, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 19 minutes and 47 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f0e24526-a780-474d-8d11-ff9ea6bc8ebc

📥 Commits

Reviewing files that changed from the base of the PR and between 2ae0f78 and 0a11324.

📒 Files selected for processing (3)
  • packages/opencode/src/automation/scheduler.ts
  • packages/opencode/src/project/instance.ts
  • packages/opencode/test/server/automation-scheduler.test.ts
📝 Walkthrough

Walkthrough

Adds a pluggable AutomationScheduler with a Clock/TaskRuntime, extends run helpers to accept explicit timing and track runIDs, wires the scheduler into instance/server/tool lifecycle paths, and adds comprehensive FakeClock-driven tests for one-shot, recurring, missed, and cancellation behaviors.

Changes

Automation Scheduler Implementation

Layer / File(s) Summary
Automation run timing helpers
packages/opencode/src/automation/index.ts
Active-run tracking now stores runID; stopRun accepts timing options; stopRunByID/stopActiveRun use runID checks; added hasActiveRun, hasRunTriggeredAtOrAfter, completedRunCount, and recordStoppedRun; executeRun records active run and finally cleanup only clears matching runID.
AutomationScheduler API, contracts, and core runtime
packages/opencode/src/automation/scheduler.ts
Introduces AutomationScheduler namespace: Clock with async sleep, Task/TaskRuntime, computeNextFireAt, and make() factory implementing timer/task maps, cancel/reschedule, missed-schedule handling, active-run coordination, unattended executor invocation, and shutdown/stopOwnedRuns.
Scheduler singleton and instance integration
packages/opencode/src/automation/scheduler.ts
Provides current()/install() singleton integration and helpers to stop runs owned by the current instance, a directory, or all directories.
Instance dispose maintenance wiring
packages/opencode/src/project/instance.ts
Adds lazy scheduler importer and invokes stopCurrentOwnedRuns / stopDirectoryOwnedRuns / stopAllOwnedRuns from Instance.dispose/Instance.disposeDirectory/Instance.disposeAll when mode is maintenance.
Server routes and tool integration
packages/opencode/src/server/instance/automation.ts, packages/opencode/src/tool/automate.ts
Calls AutomationScheduler.current() at start of create/update/pause/resume handlers; cancels scheduled timers on delete using tombstone id; tool automate calls current() during execute before creating definitions.
Deterministic scheduler tests and route tests
packages/opencode/test/server/automation-scheduler.test.ts, packages/opencode/test/server/automation-routes.test.ts
Adds FakeClock/OversleepClock, test helpers (withAutomation, input builders, polling), and an extensive test suite validating one-shot firing, recurring anchoring/non-overlap, missed schedules, cancellation on disposal, writer-contention behavior, and route-level scheduler cancel/reschedule assertions.

Sequence Diagram

sequenceDiagram
  participant Clock
  participant Scheduler as AutomationScheduler
  participant Automation as Automation Runtime
  participant Executor as Run Executor
  Clock->>Scheduler: sleep finishes / timer fires
  Scheduler->>Automation: evaluate schedule & active run
  alt Active run awaiting input
    Scheduler->>Automation: recordStoppedRun(previous_run_awaiting_input)
    Automation-->>Scheduler: stopped run recorded
  else No active run
    Scheduler->>Executor: fork unattended execution (AbortSignal)
    Executor->>Automation: execute run and publish terminal state
    Automation-->>Scheduler: run finished event
  end
  alt Recurring and eligible
    Scheduler->>Scheduler: computeNextFireAt and schedule next via Clock.sleep
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Astro-Han/pawwork#983: Both PRs modify packages/opencode/src/automation/index.ts’s active-run tracking and stop semantics, intersecting on runID and abort behavior.
  • Astro-Han/pawwork#960: Related scheduler wiring into automation routes and tool execution paths; overlaps with singleton/instance scheduler integration.

Poem

🐰 I nudge the Clock and watch it hum,
Timers hop and runs become,
Stops recorded, races tamed,
FakeClock hops — the tests proclaimed,
Hooray for scheduled dawn!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add automation scheduler timers' accurately describes the main change: adding scheduler functionality with timer support to the automation system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering all major template sections including summary, rationale, related issue, human review status, review focus, risk notes, verification steps, and completed checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/i950-automation-pr3-scheduler

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested priority: P2 (includes non-doc, non-test paths outside the low-risk bucket).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@Astro-Han Astro-Han added the app Application behavior and product flows label May 29, 2026
@github-actions github-actions Bot removed the app Application behavior and product flows label May 29, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an AutomationScheduler to manage and execute scheduled automation runs (both one-shot and recurring interval-based), integrating it with the existing automation routes and adding comprehensive unit tests. The review feedback highlights three key issues in the scheduler implementation: first, using triggeredAt instead of clock.now() for scheduling the next run could cause a rapid cascade of timers if the system falls behind; second, because running is initialized to true, the start() method's early return guard prevents existing automations from being scheduled on startup; and third, one-shot automations lack a completion state, which may lead to duplicate executions if the scheduler is restarted or updated after they have already fired.

Comment thread packages/opencode/src/automation/scheduler.ts Outdated
Comment thread packages/opencode/src/automation/scheduler.ts
Comment thread packages/opencode/src/automation/scheduler.ts Outdated
@Astro-Han
Astro-Han force-pushed the codex/i950-automation-pr3-scheduler branch from fd9c3b8 to 3527751 Compare May 29, 2026 14:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/opencode/src/automation/scheduler.ts (2)

60-81: ⚡ Quick win

Add defensive error handling for deleted automations.

The fire() function calls Automation.runNowExecuting(automationID, ...) at line 67 without catching potential errors. If the automation is deleted between the timer firing (line 61) and the runNowExecuting call, Automation.get(id) (called within runNow at line 571 of index.ts) will throw NotFoundError, resulting in an unhandled promise rejection.

While the proper deletion flow calls scheduler.cancel(automationID) before deletion, a narrow race window exists where the timer callback executes after deletion completes.

Wrap the runNowExecuting call in a try-catch block to gracefully handle this edge case:

🛡️ Defensive error handling
 const fire = (automationID: string, triggeredAt: number) => {
   timers.delete(automationID)
   if (Automation.hasActiveRun(automationID)) {
     const stopped = Automation.recordStoppedRun(automationID, "previous_run_awaiting_input", { now: triggeredAt })
     void Automation.publishRunUpdated(stopped)
     return
   }
+  try {
     Automation.runNowExecuting(automationID, {
       executor: async (input) => {
         try {
           return await executor(input)
         } finally {
           const latest = Automation.get(input.definition.id)
           if (latest.kind === "recurring" && latest.rhythm.kind === "interval" && !latest.paused) {
             schedule(latest, clock.now() + latest.rhythm.everyMs)
           }
         }
       },
       attendance: "unattended",
       now: triggeredAt,
     })
+  } catch (error) {
+    // Automation was deleted; timer already removed, nothing to do
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/automation/scheduler.ts` around lines 60 - 81, The
timer callback fire should defensively handle the case where the automation was
deleted between timers.delete and invoking Automation.runNowExecuting: wrap the
call to Automation.runNowExecuting(automationID, ...) in a try/catch, catch
NotFoundError (or the error thrown by Automation.get) and silently return (or
log at debug) instead of letting the promise reject; keep the existing finally
behavior inside the executor intact and only suppress the specific
deletion-related error while rethrowing or propagating other unexpected errors.

40-45: 💤 Low value

Consider renaming for clarity.

The function name nextFireAt computes "next fire time from a given point," but for interval automations it simply returns from + everyMs without considering the definition's nextFireAt field. While the usage is correct (callers pass completion time or clock.now() as from), the name might mislead readers into expecting the function to return the pre-computed definition.nextFireAt for recurring automations.

Consider renaming to computeNextFireAt or calculateFireTime to better convey that this is a calculation helper rather than a field accessor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/automation/scheduler.ts` around lines 40 - 45, Rename
the helper function nextFireAt to a clearer verb-named function like
computeNextFireAt (or calculateFireTime) to indicate it computes a value rather
than returning a stored field; update the exported function name export function
nextFireAt(definition: Automation.Definition, from: number): number | null to
export function computeNextFireAt(...) and update all call sites to use
computeNextFireAt, leaving logic unchanged (still handling definition.paused,
oneshot via definition.fireAt, interval via from + definition.rhythm.everyMs)
and keeping references to definition.nextFireAt intact where callers
intentionally use the stored field.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/opencode/src/automation/scheduler.ts`:
- Around line 60-81: The timer callback fire should defensively handle the case
where the automation was deleted between timers.delete and invoking
Automation.runNowExecuting: wrap the call to
Automation.runNowExecuting(automationID, ...) in a try/catch, catch
NotFoundError (or the error thrown by Automation.get) and silently return (or
log at debug) instead of letting the promise reject; keep the existing finally
behavior inside the executor intact and only suppress the specific
deletion-related error while rethrowing or propagating other unexpected errors.
- Around line 40-45: Rename the helper function nextFireAt to a clearer
verb-named function like computeNextFireAt (or calculateFireTime) to indicate it
computes a value rather than returning a stored field; update the exported
function name export function nextFireAt(definition: Automation.Definition,
from: number): number | null to export function computeNextFireAt(...) and
update all call sites to use computeNextFireAt, leaving logic unchanged (still
handling definition.paused, oneshot via definition.fireAt, interval via from +
definition.rhythm.everyMs) and keeping references to definition.nextFireAt
intact where callers intentionally use the stored field.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de879331-b65d-4e15-b598-bdf7c7843167

📥 Commits

Reviewing files that changed from the base of the PR and between 6b813b2 and 3527751.

📒 Files selected for processing (5)
  • packages/opencode/src/automation/index.ts
  • packages/opencode/src/automation/scheduler.ts
  • packages/opencode/src/server/instance/automation.ts
  • packages/opencode/test/server/automation-routes.test.ts
  • packages/opencode/test/server/automation-scheduler.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/opencode/src/automation/scheduler.ts (1)

171-179: ⚡ Quick win

waitUntil loop ignores the abort signal.

The wait loop only checks clock.now() < fireAt. When signal is aborted (cancel/reschedule), liveClock.sleep resolves immediately, so the loop keeps re-sleeping and correctness depends entirely on the TaskRuntime also interrupting the fiber. A pluggable runtime that only aborts the signal (the TaskRuntime contract only guarantees interrupt()) would spin tightly. Separately, the post-loop guard tasks.has(automationID) can match a newly scheduled task after a reschedule, letting a stale fiber call fire. Gate on signal.aborted to make abort authoritative.

🔒️ Proposed fix
     Effect.gen(function* () {
       while (clock.now() < fireAt) {
+        if (signal.aborted) return
         const delayMs = Math.max(0, fireAt - clock.now())
         yield* Effect.promise(() => clock.sleep(Math.min(delayMs, MAX_TIMER_DELAY_MS), signal))
       }
-      if (!running || !tasks.has(automationID)) return
+      if (!running || signal.aborted || !tasks.has(automationID)) return
       fire(automationID, fireAt)
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/automation/scheduler.ts` around lines 171 - 179, The
waitUntil loop ignores the AbortSignal so a cancelled/rescheduled task can spin
or let a stale fiber call fire; update waitUntil (and its loop condition) to
abort early by checking signal.aborted in the while condition and after waking,
and before invoking fire(automationID, fireAt) also verify the signal is not
aborted and that the currently scheduled task still matches this timer (e.g.
tasks.has(automationID) AND the task's scheduled time equals fireAt) so a
rescheduled task won't be overwritten; adjust references in waitUntil,
clock.sleep, running, tasks.has/get, and fire accordingly to make the
AbortSignal authoritative.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/src/project/instance.ts`:
- Line 130: The call to stop helpers like stopCurrentOwnedRuns (via scheduler())
can throw synchronously and should not abort disposal; wrap the stop invocation
in a best-effort try/catch so teardown still proceeds and errors are logged.
Locate the three sites where scheduler().stopCurrentOwnedRuns,
scheduler().stopDirectoryOwnedRuns, and scheduler().stopAllOwnedRuns are invoked
(e.g., inside disposeInstance/disposeDirectory/disposeAllLoadedInstances) and
change each to call the stop helper inside a try { await (or call) scheduler();
/*call stop helper*/ } catch (err) { logger.error(...) } (ensure you use the
existing logger/processLogger and continue disposal) so any synchronous throw is
caught and cleanup always runs.

---

Nitpick comments:
In `@packages/opencode/src/automation/scheduler.ts`:
- Around line 171-179: The waitUntil loop ignores the AbortSignal so a
cancelled/rescheduled task can spin or let a stale fiber call fire; update
waitUntil (and its loop condition) to abort early by checking signal.aborted in
the while condition and after waking, and before invoking fire(automationID,
fireAt) also verify the signal is not aborted and that the currently scheduled
task still matches this timer (e.g. tasks.has(automationID) AND the task's
scheduled time equals fireAt) so a rescheduled task won't be overwritten; adjust
references in waitUntil, clock.sleep, running, tasks.has/get, and fire
accordingly to make the AbortSignal authoritative.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a4a12da-14b7-4af2-9b99-cc999a901030

📥 Commits

Reviewing files that changed from the base of the PR and between 3527751 and b743101.

📒 Files selected for processing (7)
  • packages/opencode/src/automation/index.ts
  • packages/opencode/src/automation/scheduler.ts
  • packages/opencode/src/project/instance.ts
  • packages/opencode/src/server/instance/automation.ts
  • packages/opencode/src/tool/automate.ts
  • packages/opencode/test/server/automation-routes.test.ts
  • packages/opencode/test/server/automation-scheduler.test.ts

Comment thread packages/opencode/src/project/instance.ts Outdated
@Astro-Han
Astro-Han merged commit 5e43399 into dev May 30, 2026
26 checks passed
@Astro-Han
Astro-Han deleted the codex/i950-automation-pr3-scheduler branch May 30, 2026 09:47
Astro-Han added a commit that referenced this pull request Jun 2, 2026
Closes the backend gaps left after PR1-5 so the PR6/PR7 frontend slice can
build on a complete, stable automation contract.

Goal / change boundary:
- model { providerID, modelID } now required on every AutomationDefinition;
  runner passes it through so runs no longer depend on the runtime model
  fallback chain (which drifts across restarts).
- variant? optional reasoning/effort, validated against the live provider
  catalog on create/update; invalid variant returns 422 instead of failing
  late in the run.
- stop.kind === "condition" rejected at create/update with structured
  { field: "stop", message: "unsupported_stop_condition" } (scheduler never
  schedules condition stops, so accepting them only leaked dead UI surface).
- Derived fields nextFireAt / nextFires / failureStreak populated at
  create/update and refreshed after every terminal run; scheduler re-publishes
  automation.definition.updated with a bumped revision for global-sync.
- cron validation consolidated into src/automation/cron.ts; scheduler and
  derived both consume it.
- automate tool schema picks up model/variant + Provider-backed create-time
  validation.
- Migration 20260601100000 drops pre-release rows so model can be NOT NULL.

Verification:
- CI green on 5e177ba (unit-opencode flake on an unrelated VCS-routes
  20s timeout cleared on rerun).
- Local: 129 automation tests (routes/runner/scheduler/event-fixtures/tool/
  cron) + 35 session processor tests pass.

Review follow-ups addressed:
- codex: scheduler self-loop guard keyed by id:revision (not id alone).
- P2: recordRunOutcome retries on ConflictError instead of silently dropping
  the run outcome.
- CodeRabbit: 422 create test uses a guaranteed-invalid model; fixed sleeps
  replaced with terminal-state polling.
- Test seam for the ConflictError path moved off the public Automation API
  into an internal __test_hooks module.

Residual risk / deferred (tracked, not blocking PR6):
- needs_user_input / loop_gate run-error codes remain reserved; producing
  them needs prompt-loop semantics changes.
- Session.automationID reverse lookup deferred (session-contract migration).
- stop=condition kept in the schema layer for SDK shape parity but rejected
  at validate time; collapses once a condition evaluator lands.

Linked: issue #950; follows PR #960 #983 #984 #998 #1004; unblocks PR6/PR7.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request harness Model harness, prompts, tool descriptions, and session mechanics P1 High priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant