Repository navigation
feat(mothership): run scheduled jobs as durable unattended chat turns - #9331
Conversation
A scheduled job's occurrence must outlive the Sim process driving it. The headless lifecycle the wake door uses takes no controller lease, so a pod death leaves its run unrecoverable. An unattended turn now admits like a typed send (user message, run and recovery intent in one commit under the chat lock), runs detached from any HTTP response, and records a new unattended_stream recovery kind. Takeover restores it non-interactive with the secret authority it was admitted with, instead of the interactive default, and the abandoned-run sweep, stream liveness and billing-admission recording treat both durable kinds alike. Durable start now also forwards a caller's billing-admission hook, so a job can key its per-run budget on the run's billing request. The integration test kills the turn's controller three times mid-run; it fails on a headless implementation, on a sweep blind to the new kind, and on a takeover that restores the interactive default.
Scheduled jobs return as Sim-owned schedules whose occurrences run as durable chat turns. A job's run record (job_execution_logs) gains the occurrence it runs (unique per schedule), its outcome and summary, and the billing request its charges settle under; a schedule gains a per-run spend cap. Indexes are built concurrently. Every job row written for the retired Go runner is archived, never deleted, so none can start once the tick claims jobs again. The archive is a script migration committing per keyed batch, so it finishes on any row count and resumes after a crash.
Behind MOTHERSHIP_JOBS_ENABLED (off by default, and to stay off in production until unattended runs hold production effects for approval), the schedule tick gains a job lane: - Claim: each due job's occurrence is recorded (queued) and the schedule advances past it in one transaction, skipping locked rows, so two ticks record it once and a crash never leaves a stale claim. Missed occurrences collapse into one; cadence is never under 15 minutes; a one-shot job (no cron) runs once and completes; max_runs completes. - Dispatch: the occurrence starts as an unattended durable turn in the job's chat under its occurrence id (UUIDv5 of schedule and instant), so every retry finds the turn it admitted. A busy chat skips the occurrence; a payer over its plan limit skips it; an owner who lost access turns the job off. Only secrets a person named for the job mount. - Reconcile: a queued occurrence whose dispatch was lost is dispatched again after two minutes; a running one whose turn ended is finalized; one past six hours is stopped. - Finalize: exactly once under a row lock. A reported outcome is kept; a turn that ended well without one is not_met, summarized by its last reply. Three failed or interrupted runs in a row turn the job off and email its people; a met until_complete job completes. - Budget: each billing callback records the run's cumulative settled cost on its record (keyed by the run's billing request), and a run at its cap pauses at its next step with a job_run_budget card. The cost is cumulative per billing request, which recovered legs reuse, so the cap survives worker hand-offs and Sim takeovers.
…the shared Stop delivery
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
There was a problem hiding this comment.
1 issue found and verified against the latest diff
Confidence score: 3/5
- In
apps/sim/lib/mothership/jobs/dispatch.ts, a crash after the run becomes recoverable but beforebillingAdmissionis persisted leaves hosted recovery unable to resume the occurrence. Persist the admission before marking the run recoverable.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/sim/lib/mothership/jobs/dispatch.ts">
<violation number="1" location="apps/sim/lib/mothership/jobs/dispatch.ts:90">
P2: The job run becomes recoverable before its billing admission is persisted. A crash in that gap leaves hosted recovery without `billingAdmission`, which `runCopilotLifecycle` rejects instead of resuming the occurrence; persist the admission before exposing the run to recovery or make this pre-dispatch recovery retryable.</violation>
</file>
Heads up: you’re close to your flex budget. Increase your flex budget so reviews don’t pause.
Turn on auto-fix | Re-trigger cubic
[Medium impact] The changes appear safe to merge; no new actionable issue was found. SummaryThe latest changes close running job occurrences whose chat run was removed. They now finish as
|
…hat protocol graph
…s and failure counts durable
|
@greptile |
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 49 files
Heads up: you’re close to your flex budget. Increase your flex budget so reviews don’t pause.
Turn on auto-fix | Re-trigger cubic
…umber of deleted occurrences
|
@greptile |
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 49 files
Heads up: you’re close to your flex budget. Increase your flex budget so reviews don’t pause.
Turn on auto-fix | Re-trigger cubic
|
@greptile |
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 49 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Heads up: you’re close to your flex budget. Increase your flex budget so reviews don’t pause.
Turn on auto-fix | Re-trigger cubic
Summary
Restores the runtime for scheduled agent jobs on the TypeScript worker. The schedule tick claims each due job occurrence and runs it as a durable, leased chat turn in the job's own chat. There is no creation surface yet (no API, UI or agent command), so this PR ships the runtime only, and the lane has nothing to run until that surface lands.
request/lifecycle/unattended.ts,chat/application/admit-unattended-turn.ts). They are admitted like a typed send: the user message, the run and its recovery intent commit together under the chat lock. The new recovery kind isunattended_stream, and abandoned-run takeover drives one on after its Sim process dies, keeping it non-interactive and mounting only the secrets the job recorded. The headless wake lifecycle cannot do this: it holds no controller lease, so nothing can continue its run once the pod is gone.job_execution_logs,queued) and advances the schedule. The occurrence id is a UUIDv5 of the schedule and the instant, and it is the turn's message, stream and run id. Two ticks, or a reconciler retry, therefore admit the turn at most once. A reconciler re-dispatches lost dispatches, finalizes ended turns exactly once under a row lock, and stops a run at its six-hour limit through the same Stop delivery a person's Stop uses.job_run_budgetcard;max_runs;met,not_met,blocked), not a tool call scanned out of a response.0437adds the occurrence columns plus three indexes, builtCONCURRENTLY. Script migration0036archives (never deletes) every job row the retired Go runner wrote. It commits per batch and is idempotent.Why this runs without a flag
source_type = 'job'row: schedule deploys insert workflow schedules only, and the job create paths were removed earlier.0036runs in the migrations step, after the SQL migrations and before the app starts. The claim only reads rows that are unarchived and not disabled or completed.jobLaneWorkQuery. Its twoEXISTSlegs probe the partial indexesworkflow_schedule_due_job_idxandjob_execution_logs_open_idx. It claims nothing and reconciles nothing. An integration test seeds 2,000 archived jobs and 5,000 finished logs, then asserts no claim, no reconcile, no sequential scan and fewer than 20 buffer reads.COPILOT_API_KEYorREDIS_URLthe lane returns before any query (unit test).Companion: simstudioai/mothership#818
Type of Change
Testing
lib/mothership/request/lifecycle/unattended.integration.ts(real Postgres and Redis): one unattended turn survives three controller deaths mid-run and finishes once. It ends with one user message, one assistant message and one run row, and every recovered leg stays non-interactive with the job's secrets.lib/mothership/jobs/lane.integration.tscovers:max_runs;until_complete;lib/mothership/jobs/lane.test.tschecks that the lane makes no DB calls when its prerequisites are missing.packages/db/script-migrations/0036_archive_legacy_scheduled_jobs.integration.tscovers batching, idempotency, and keeping completed jobs completed.start.ts,recovery-config.ts,recover-stream.tsandadmit-turn.ts: the recovery kind is now explicit, and the chat-move step is shared with typed sends.Checklist