feat(deployments): 使用 River 实现定时调度 - #219
Conversation
📝 WalkthroughWalkthroughThis change adds cron-based scheduled deployments with River-backed execution, persistent schedule revisions and cursors, transactional webhook delivery, scheduled-run preparation, typed trigger responses, startup wiring, and integration coverage. ChangesScheduled deployment scheduling
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant API
participant Database
participant River
participant Scheduler
participant WebhookOutbox
API->>Database: persist schedule and enqueue occurrence
River->>Scheduler: execute scheduled occurrence
Scheduler->>Database: validate state and apply run
Database->>WebhookOutbox: enqueue lifecycle events
Scheduler->>River: advance or retry occurrence
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 64c21deb1e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
main.go (1)
56-62: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winDocument the
cmd/migraterequirement whenAutoMigrateis disabled.When
database.auto_migrateis off,cmd/migrate upstill runs Goose migrations anddeployments.MigrateRiverbeforeoma-serverstarts. If an operator starts the server withauto_migrate: falsebefore applying migrations,deploymentScheduler.Start(ctx)fails with missing River tables and only reportsstart deployment scheduler. Add this ordering to the runbook or run both migration steps unconditionally before starting the scheduler.🤖 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 `@main.go` around lines 56 - 62, The AutoMigrate-disabled startup path must ensure both Goose and River migrations run before deploymentScheduler.Start(ctx). Either document in the runbook that operators must run cmd/migrate up before starting oma-server, or move database.Migrate and deployments.MigrateRiver outside the cfg.Database.AutoMigrate guard so both execute unconditionally.
🧹 Nitpick comments (22)
tests/deployments_api_test.go (1)
720-768: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove the success subtest before the failure subtests, or move the failure subtests after it.
The added block places
failure auto pause rolls back...,failure scheduled root agent archive..., andfailure agent archive rolls back...beforeagent API archives deployments with webhook outbox. The coding guideline requires failure scenarios first and success scenarios after. Reorder the new subtests so allfailure ...subtests precede this success subtest.As per coding guidelines: "Order tests with failure scenarios before success scenarios".
🤖 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 `@tests/deployments_api_test.go` around lines 720 - 768, Reorder the subtests in the test suite so the failure scenarios “failure auto pause rolls back...”, “failure scheduled root agent archive...”, and “failure agent archive rolls back...” appear before the successful “agent API archives deployments with webhook outbox” subtest. Keep each subtest’s implementation unchanged.Source: Coding guidelines
docs/design/be/deployments-api-contract.md (1)
88-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReference the source of the 14 paused-reason error types.
The count
14 类 paused-reason errorwill drift when the code list changes. Name the Go constant or slice that holds the list so readers can verify it.🤖 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 `@docs/design/be/deployments-api-contract.md` at line 88, Update the deployment API contract documentation to reference the Go constant or slice that defines the 14 public paused-reason error types instead of relying only on the numeric count. Keep the existing behavior description unchanged, and use the exact source symbol name so readers can verify the list.web/src/features/managed-agents/ManagedAgentsPage.resources.suite.tsx (1)
2187-2187: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueAlign the deployment schedule with the real response shape.
internal/deployments/cron_test.gousesexpression, while this fixture usesQuickstartDeploymentInput’scron_expression;DeploymentApiResponse.schedulehas no typed shape. Use the response format or remove this fixture assignment if it is not exercised.🤖 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 `@web/src/features/managed-agents/ManagedAgentsPage.resources.suite.tsx` at line 2187, Update the fixture assignment near api.resources.deployments in the managed agents resources suite to match the real DeploymentApiResponse schedule shape by using the response’s expression field instead of QuickstartDeploymentInput’s cron_expression; if this schedule is not exercised, remove the assignment.internal/webhooks/enqueuer_test.go (2)
16-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove this success-scenario test after the failure-scenario test.
TestPrepareDeliveryEventPreservesOutboxDataasserts the success path. The existing test that usesfailingEnqueueStoreat Line 62 asserts a failure path. Place the failure-scenario test first.As per coding guidelines: "Order tests with failure scenarios before success scenarios".
🤖 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 `@internal/webhooks/enqueuer_test.go` around lines 16 - 37, Reorder the tests in the relevant test file so the existing failure-scenario test using failingEnqueueStore appears before TestPrepareDeliveryEventPreservesOutboxData. Do not change either test’s implementation or assertions.Source: Coding guidelines
22-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtend the assertions to cover the fields the test name claims.
The test name states that outbox data is preserved. The assertions cover
FallbackEnabled,EventType,CreatedAt, andData.IDonly.event.ID,event.Data.Type,event.Data.WorkspaceID, andevent.Data.OrganizationIDare the fields that the scheduler's transactional outbox writes depend on, and they are not checked.Assert those fields as well, and split the combined condition so a failure names the field.
💚 Proposed additional assertions
- if !deliveryEvent.FallbackEnabled || deliveryEvent.EventType != "deployment_run.started" || - event.CreatedAt != "2026-08-07T01:02:03Z" || event.Data.ID != "drun_test" { - t.Fatalf("PrepareDeliveryEvent() = %+v, event = %+v", deliveryEvent, event) - } + if !deliveryEvent.FallbackEnabled { + t.Errorf("FallbackEnabled = false, want true") + } + if deliveryEvent.EventType != "deployment_run.started" { + t.Errorf("EventType = %q, want %q", deliveryEvent.EventType, "deployment_run.started") + } + if event.CreatedAt != "2026-08-07T01:02:03Z" { + t.Errorf("CreatedAt = %q, want %q", event.CreatedAt, "2026-08-07T01:02:03Z") + } + if !strings.HasPrefix(event.ID, "wevt_") { + t.Errorf("ID = %q, want prefix %q", event.ID, "wevt_") + } + if event.Data.ID != "drun_test" || event.Data.Type != "deployment_run.started" || + event.Data.WorkspaceID != "workspace_test" || event.Data.OrganizationID != "org-uuid" { + t.Errorf("Data = %+v", event.Data) + }Add
"strings"to the imports.🤖 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 `@internal/webhooks/enqueuer_test.go` around lines 22 - 36, Extend the assertions in the PrepareDeliveryEvent test to verify event.ID, event.Data.Type, event.Data.WorkspaceID, and event.Data.OrganizationID alongside the existing fields. Split the combined condition into field-specific assertions so failures identify the mismatched field, and add the strings import only if needed for the expected workspace or organization value checks.internal/deployments/scheduler.go (3)
217-220: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winA permanently invalid stored schedule blocks the deployment forever.
nextAfterScheduledreturns an error for a schedule that cannot be parsed.Workreturns that error, so River retries the job until it is discarded. The schedule cursor never advances, andreconcileskips the same deployment on every pass becausejitteredTriggerAtalso fails. The deployment then staysactivewith a stalenext_scheduled_atand no operator-visible run.Record a failure run and auto-pause the deployment for this case, in the same way as other non-retryable preparation failures.
🤖 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 `@internal/deployments/scheduler.go` around lines 217 - 220, Update the invalid-schedule handling in Work around nextAfterScheduled so an unparseable stored schedule is treated as a non-retryable preparation failure: record a failure run and automatically pause the deployment, matching the existing handling for other non-retryable preparation failures. Ensure Work does not return the parse error for River retry, and keep reconcile from repeatedly skipping the unchanged active deployment.
263-288: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy liftThe next occurrence is enqueued only by the 30-second reconcile loop.
After
ApplyScheduledOccurrenceadvancesnext_scheduled_at, no job is inserted for the new occurrence. The next job appears on the following reconcile tick. This adds up toscheduleReconcileIntervalof latency to every occurrence, and it makes minute-level cron schedules imprecise.Insert the follow-up job directly after a successful apply, and keep the reconcile loop as the recovery path.
♻️ Sketch of the follow-up insert
if err != nil { return err } + if nextScheduledAt != nil { + deployment.ScheduleRevision = args.ScheduleRevision + deployment.NextScheduledAt = nextScheduledAt + if err := w.enqueueNext(ctx, deployment); err != nil { + w.logger.ErrorContext(ctx, "enqueue next deployment occurrence", + "deployment_id", deployment.ExternalID, "error", err) + } + } return nil
enqueueNextneeds access to the River client, so pass the client or a small inserter interface intoscheduledDeploymentWorker.🤖 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 `@internal/deployments/scheduler.go` around lines 263 - 288, After a successful ApplyScheduledOccurrence call in the scheduledDeploymentWorker flow, immediately enqueue the newly computed nextScheduledAt occurrence instead of waiting for reconciliation. Update scheduledDeploymentWorker or its dependencies to provide enqueueNext with the River client or a minimal inserter interface, while preserving the reconcile loop as the recovery path and existing error handling.
82-94: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value
Startblocks application startup on a full reconcile.
backfillNextScheduledAtandreconcileiterate every scheduled deployment and perform one database write or job insert per deployment. Both run synchronously beforeclient.Start. With a large number of scheduled deployments, this delays process startup, and a single insert error aborts startup througherrors.Join.Consider running the initial reconcile in the background loop, and treating per-deployment errors as logged failures instead of startup failures.
🤖 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 `@internal/deployments/scheduler.go` around lines 82 - 94, The DeploymentScheduler.Start method currently blocks startup and propagates per-deployment failures from backfillNextScheduledAt and reconcile. Move the initial backfill/reconcile work into the background reconciliation flow so client.Start executes without waiting, and handle individual deployment errors by logging them while allowing the loop to continue rather than returning them as startup errors.main.go (1)
136-142: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winThe stop timeout is not aligned with the River client timeouts.
Stopgets 10 seconds.internal/deployments/scheduler.gosetsSoftStopTimeout: 10 * time.SecondandJobTimeout: 2 * time.Minute. The soft-stop phase alone consumes the whole budget, so the hard-stop phase never gets time, andStopreports a timeout error on every shutdown that has an in-flight job.Set the shutdown context longer than
SoftStopTimeout, for example 20 seconds.In-flight work is safe because
ApplyScheduledOccurrenceruns in one transaction and the job is retried, but the recurring error log is misleading.♻️ Proposed change
defer func() { - stopCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + stopCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() if err := deploymentScheduler.Stop(stopCtx); err != nil { logger.Error("stop deployment scheduler", "error", err) } }()🤖 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 `@main.go` around lines 136 - 142, Increase the shutdown context timeout in the deferred cleanup around deploymentScheduler.Stop from 10 seconds to a value longer than the scheduler’s SoftStopTimeout, such as 20 seconds, so the hard-stop phase can complete without recurring timeout errors.internal/deployments/execution.go (1)
31-43: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winPreparation errors are reported with a misleading error type.
sessionEventsFromInitialEvents,sessionResourcesFromDeployment, andhttpapi.MarshalRawreturn plain errors. Ininternal/deployments/scheduler.goat Line 257, every non-retryable error fromprepareDeploymentRunis recorded assession_resource_not_found_error. A malformedInitialEventspayload or a marshal failure then produces a wrong error type in the run record, andshouldAutoPausepauses the deployment with that wrong reason.Return a classified error from
prepareDeploymentRun, or wrap each failure source with its own error type before it reachesrecordFailure.🤖 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 `@internal/deployments/execution.go` around lines 31 - 43, The prepareDeploymentRun error paths for sessionEventsFromInitialEvents, sessionResourcesFromDeployment, and httpapi.MarshalRaw currently return unclassified errors that scheduler.go records as session_resource_not_found_error. Update prepareDeploymentRun to return a classified error, or wrap each failure with an appropriate distinct error type, so recordFailure and shouldAutoPause receive the correct failure reason for malformed events, resource preparation, and marshal failures.internal/deployments/handler.go (4)
620-638: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winCompute the next occurrence from the row that the transaction unpauses.
Line 620 reads the deployment outside the transaction. Line 625 derives
nextfrom that stale copy. A concurrent update toschedulebetween the read andUnpauseDeploymentTxmakes the persistednext_scheduled_atinconsistent with the stored schedule, and the enqueued River job then targets the wrong occurrence. Move the read and the calculation inside the transaction callback, or haveUnpauseDeploymentTxderive the next occurrence from the locked row.🤖 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 `@internal/deployments/handler.go` around lines 620 - 638, Move the deployment read and nextScheduledAt calculation into the transaction callback used by the unpause flow, ensuring they operate on the transaction’s current or locked row before calling UnpauseDeploymentTx and enqueueScheduledOccurrenceTx. Keep error logging and HTTP error responses consistent, and ensure the persisted next_scheduled_at and enqueued job derive from the same schedule.
775-862: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSplit
validateRunReferencesinto per-resource validators.The function validates six reference kinds in one body. Its cyclomatic complexity is close to the 30 limit for Go functions, so any later branch pushes it over. Extract
validateAgentReferences,validateSkillReferences,validateVaultReferences, andvalidateResourceReferences, then call them in sequence.As per coding guidelines: "Respect complexity budgets: Go functions must remain at cyclomatic complexity 30 or below".
🤖 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 `@internal/deployments/handler.go` around lines 775 - 862, Split validateRunReferences into the requested helpers: validateAgentReferences, validateSkillReferences, validateVaultReferences, and validateResourceReferences. Move each corresponding validation block into its helper, preserving existing error classification, ordering, and return behavior, then have validateRunReferences invoke them sequentially and continue only when each succeeds.Source: Coding guidelines
289-297: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename the local variable to avoid shadowing the
nextScheduledAtfunction.Line 289 declares a local variable with the same name as the package-level function. The function becomes unreachable for the rest of
create. Rename the variable, for examplenextRunAt.♻️ Proposed rename
- nextScheduledAt, err := nextScheduledAt(schedule, now) + nextRunAt, err := nextScheduledAt(schedule, now) if err != nil { writeBadRequest(w, r, err) return } revision := int64(0) - if nextScheduledAt != nil { + if nextRunAt != nil { revision = 1 }Update the struct field assignment at line 319 as well:
NextScheduledAt: nextRunAt,🤖 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 `@internal/deployments/handler.go` around lines 289 - 297, Rename the local result variable in create from nextScheduledAt to nextRunAt so it does not shadow the nextScheduledAt function, update its nil check and revision logic, and use nextRunAt in the NextScheduledAt struct field assignment.
822-861: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftBatch the vault, file, and memory-store lookups.
Lines 828-836 and 843-861 run one database query per reference, in sequence, on the request thread. The deployment resource limit permits up to 500 file resources, so a single
POST /v1/deployments/{id}/runcan issue hundreds of sequential round trips before any work starts. Add batch lookups by external ID for vaults, files, and memory stores, then validate the returned set.🤖 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 `@internal/deployments/handler.go` around lines 822 - 861, Replace the sequential lookups in the vault and resources validation flow with batch-by-external-ID queries for vaults, files, and memory stores, grouping resource IDs by type before querying. Validate the returned records as a set, preserving missing-reference and archived-record handling through classifyReferenceFailure, and keep the existing invalid JSON behavior unchanged.internal/deployments/scheduler_test.go (1)
1-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a test for the worker success path.
This file covers failure classification, auto-pause selection, and failure webhook inputs. It does not cover the successful scheduled execution path, which creates the session, the deployment run, and the outbox rows, and then advances the schedule cursor. The PR description lists this path as a review focus. Do you want me to draft the test?
🤖 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 `@internal/deployments/scheduler_test.go` around lines 1 - 9, Add a test in the scheduler test suite covering the successful scheduled execution path: verify session, deployment run, and outbox rows are created and the schedule cursor advances. Reuse the existing test fixtures, helpers, and success-path symbols in the scheduler implementation, while preserving the current failure, auto-pause, and webhook tests.internal/db/deployment_mapper_test.go (1)
168-177: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAssert that
AdvanceScheduleleavesschedule_revisionunchanged.The distinction between
AdvanceScheduleandPauseAfterScheduledRunis that only the pause statement bumpsschedule_revision. If a future edit adds the increment toAdvanceSchedule, every pending River job is invalidated after each successful run and the schedule stalls until the next reconciliation pass. The current fragments do not catch that.Add a negative assertion in the sub-test block below the table, matching the existing "include archived omits archived filter" pattern.
💚 Proposed test
t.Run("include archived omits archived filter", func(t *testing.T) { page.IncludeArchived = true bound := buildDeploymentMapperListPage(yourbatis.DialectPostgres, page) if containsSQL(bound.SQL, "archived_at IS NULL") { t.Fatalf("SQL unexpectedly filters archived deployments: %q", bound.SQL) } }) + + t.Run("advance schedule preserves the schedule revision", func(t *testing.T) { + bound := buildDeploymentMapperAdvanceSchedule(yourbatis.DialectPostgres, advance) + if containsSQL(bound.SQL, "schedule_revision = schedule_revision + 1") { + t.Fatalf("AdvanceSchedule unexpectedly bumps the schedule revision: %q", bound.SQL) + } + })🤖 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 `@internal/db/deployment_mapper_test.go` around lines 168 - 177, Extend the “advance schedule” test case for buildDeploymentMapperAdvanceSchedule with a negative SQL-fragment assertion, following the existing “include archived omits archived filter” pattern. Verify the generated statement does not contain any schedule_revision increment or update, while preserving the existing positive fragments and argument expectations.internal/db/migrations/00049_schedule_deployments_with_river.sql (1)
13-31: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoffPlan the lock impact on
deployment_runs.Static analysis flags several blocking operations on this table:
add constraint ... checkperforms a full table scan and blocks writes.NOT VALIDplus a laterVALIDATE CONSTRAINTavoids that.- Both
create indexstatements block writes.CONCURRENTLYavoids that, but requires-- +goose NO TRANSACTIONbecause goose wraps migrations in a transaction.drop column trigger_contextis metadata-only and fast, but it is irreversible for any context field other thanscheduled_at.If
deployment_runsis small in every deployed environment, the current form is acceptable. State that decision, or split the migration.🤖 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 `@internal/db/migrations/00049_schedule_deployments_with_river.sql` around lines 13 - 31, Address the lock impact in migration 00049 by either explicitly confirming that deployment_runs is small in every deployed environment and keeping the current operations, or split the migration to use NOT VALID followed by later VALIDATE CONSTRAINT and concurrent index creation with the required goose NO TRANSACTION directive. Preserve the metadata-only trigger_context removal, acknowledging its irreversible behavior.Source: Linters/SAST tools
internal/deployments/cron_test.go (2)
9-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOrder failure scenarios before success scenarios.
TestNormalizeOptionalScheduleRejectsUnsupportedSyntaxis the failure scenario. Move it aboveTestNextScheduledTimesHandlesLeapDayandTestNextScheduledTimesHandlesDST. The coding guidelines require failure tests first in*_test.gofiles.As per coding guidelines: "Order tests with failure scenarios before success scenarios".
🤖 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 `@internal/deployments/cron_test.go` around lines 9 - 72, Reorder the test functions so TestNormalizeOptionalScheduleRejectsUnsupportedSyntax appears before the successful nextScheduledTimes tests TestNextScheduledTimesHandlesLeapDay and TestNextScheduledTimesHandlesDST. Keep each test’s implementation unchanged and retain TestNormalizeOptionalScheduleAcceptsSundaySeven with the success scenarios.Source: Coding guidelines
54-65: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd a case for an unsatisfiable cron expression.
0 0 30 2 *parses successfully but never matches a date. Add it to this rejection table once the parser rejects it. See the related comment oninternal/deployments/cron.golines 114-158.💚 Proposed test case
tests := []string{ `{"type":"cron","expression":"`@daily`","timezone":"UTC"}`, `{"type":"cron","expression":"0 0 L * *","timezone":"UTC"}`, `{"type":"cron","expression":"0 0 0 * * *","timezone":"UTC"}`, + `{"type":"cron","expression":"0 0 30 2 *","timezone":"UTC"}`, }🤖 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 `@internal/deployments/cron_test.go` around lines 54 - 65, Add the unsatisfiable expression `0 0 30 2 *` to the `tests` table in `TestNormalizeOptionalScheduleRejectsUnsupportedSyntax`, preserving the existing assertion that `normalizeOptionalSchedule` returns an error for every listed expression. Ensure the parser and normalization logic reject this expression before enabling the test.internal/db/deployment_mapper.xml (1)
224-233: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winThis query has no supporting index and runs on every reconciliation tick.
The partial index
deployments_pending_schedule_idxadded in migration00049requiresnext_scheduled_at IS NOT NULL. This query requires the opposite, so PostgreSQL falls back to a sequential scan ofdeployments. The scheduler runs the query every 30 seconds across all tenants.Add a matching partial index in the migration.
⚡ Proposed index for `internal/db/migrations/00049_schedule_deployments_with_river.sql`
create index deployments_pending_schedule_idx on deployments (next_scheduled_at) where status = 'active' and archived_at is null and deleted_at is null and next_scheduled_at is not null; +create index deployments_uninitialized_schedule_idx + on deployments (uuid) + where status = 'active' + and archived_at is null + and deleted_at is null + and schedule is not null + and next_scheduled_at is null; +Add the matching
drop index deployments_uninitialized_schedule_idx;to the Down section.🤖 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 `@internal/db/deployment_mapper.xml` around lines 224 - 233, Add a partial index in migration 00049 matching the ListSchedulesMissingNextScheduledAt predicates, covering active, non-archived, non-deleted deployments with a non-null schedule and null next_scheduled_at; add the corresponding deployments_uninitialized_schedule_idx drop statement to the migration’s Down section.internal/db/webhooks.go (1)
70-98: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCache the endpoint lookup per event type.
ListActiveForEventruns once per event inside the loop. The agent-archive cascade ininternal/db/agents.gobuilds onedeployment.archivedevent per archived deployment, so the same query repeats for every deployment while holding the transaction open. This is an N+1 query on a write transaction.
hasEndpointsis also loop-invariant, so the fallback branch can move outside the loop.♻️ Proposed refactor
jobMapper := NewWebhookDeliveryJobMapper(executor) + if !hasEndpoints { + for _, event := range events { + if !event.FallbackEnabled { + continue + } + payload, err := webhookDeliveryJobPayloadJSON(event.EventType, event.Event, "") + if err != nil { + return err + } + if err := jobMapper.Insert(ctx, workspaceUUID, payload); err != nil { + return err + } + } + return nil + } + + endpointsByEventType := make(map[string][]webhookEndpointMapperRow, len(events)) for _, event := range events { - if !hasEndpoints { - if !event.FallbackEnabled { - continue - } - payload, err := webhookDeliveryJobPayloadJSON(event.EventType, event.Event, "") - if err != nil { - return err - } - if err := jobMapper.Insert(ctx, workspaceUUID, payload); err != nil { - return err - } - continue - } - - endpoints, err := endpointMapper.ListActiveForEvent(ctx, workspaceUUID, event.EventType) - if err != nil { - return err + endpoints, cached := endpointsByEventType[event.EventType] + if !cached { + var err error + endpoints, err = endpointMapper.ListActiveForEvent(ctx, workspaceUUID, event.EventType) + if err != nil { + return err + } + endpointsByEventType[event.EventType] = endpoints } for _, endpoint := range endpoints {Adjust the map value type to the actual row type returned by
ListActiveForEvent.🤖 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 `@internal/db/webhooks.go` around lines 70 - 98, Refactor the event processing loop around ListActiveForEvent and hasEndpoints to cache active endpoints by event type, using the actual row type returned by endpointMapper.ListActiveForEvent for the map values. Reuse cached results for repeated event types so each type is queried once, and move the loop-invariant fallback handling for !hasEndpoints outside the endpoint lookup path while preserving existing payload and insertion behavior.internal/deployments/cron.go (1)
68-72: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd an explicit default or make
schedule.timezonerequired.An empty
schedule.timezonepasses validation andtime.LoadLocation("")treats it as UTC, but the API does not document this fallback. Set an explicit UTC option before loading the location, or reject an omitted/blank timezone if the user must always choose a timezone.🤖 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 `@internal/deployments/cron.go` around lines 68 - 72, Update the timezone validation in the schedule parsing flow around config.Timezone and time.LoadLocation: either assign an explicit UTC default when the trimmed value is blank, or reject blank values as invalid if timezone selection is required. Ensure the chosen behavior is explicit and consistent with the API contract before loading the location.
🤖 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 `@go.mod`:
- Line 84: Upgrade the google.golang.org/grpc dependency from v1.80.0 to at
least v1.82.1 in go.mod, preserving it as an explicit requirement if needed, and
regenerate go.sum so the updated module checksums are recorded.
In `@internal/db/deployment_mapper.xml`:
- Around line 144-148: Update the schedule-change detection in
UpdateDeploymentTx before it calls UpdateByExternalID so ScheduleChanged is true
only when the normalized new schedule differs from the existing normalized
schedule. Reuse the same normalization/serialization semantics used for
persistence, then pass the comparison result into the existing deployment update
flow so schedule_revision is not incremented for equivalent schedules.
In `@internal/db/deployments.go`:
- Around line 319-326: Update SetInitialDeploymentNextScheduledAt to preserve
the rows-affected result from SetInitialNextScheduledAt and return
ErrStaleSchedule when the guarded update affects zero rows, while returning nil
for a successful update and propagating database errors. Ensure the scheduler
handles ErrStaleSchedule by skipping the River enqueue, consistent with the
worker’s existing stale-schedule handling.
In `@internal/db/migrations/00049_schedule_deployments_with_river.sql`:
- Around line 9-21: Before updating deployment_runs, add explicit validation for
existing schedule rows in the migration: fail with a clear error when
trigger_context.scheduled_at is missing or invalid, and when duplicate
deployment_uuid/scheduled_at occurrences would violate the new unique index.
Only perform the backfill and add deployment_runs_scheduled_at_check and
deployment_runs_schedule_occurrence_idx after these validations pass.
In `@internal/deployments/cron.go`:
- Around line 114-158: The cron schedule handling must reject unsatisfiable
expressions instead of persisting or returning zero times. In
internal/deployments/cron.go lines 114-158, update parseDeploymentSchedule to
reject a zero first occurrence, make nextScheduledAt and nextAfterScheduled
return nil when cron.Schedule.Next yields zero, and update jitteredTriggerAt at
lines 165-176 to handle a nil result before dereferencing next. In
internal/deployments/cron_test.go lines 54-65, add the 0 0 30 2 * UTC schedule
to TestNormalizeOptionalScheduleRejectsUnsupportedSyntax’s rejection table.
In `@web/src/features/managed-agents/resources/detail.tsx`:
- Line 680: Guard the trigger context access in the runs table so missing
trigger_context does not throw and preserves the prior fallback behavior.
Restore or reuse triggerLabel for user-facing formatting instead of rendering
raw enum values, unless the intended copy explicitly requires manual and
schedule.
---
Outside diff comments:
In `@main.go`:
- Around line 56-62: The AutoMigrate-disabled startup path must ensure both
Goose and River migrations run before deploymentScheduler.Start(ctx). Either
document in the runbook that operators must run cmd/migrate up before starting
oma-server, or move database.Migrate and deployments.MigrateRiver outside the
cfg.Database.AutoMigrate guard so both execute unconditionally.
---
Nitpick comments:
In `@docs/design/be/deployments-api-contract.md`:
- Line 88: Update the deployment API contract documentation to reference the Go
constant or slice that defines the 14 public paused-reason error types instead
of relying only on the numeric count. Keep the existing behavior description
unchanged, and use the exact source symbol name so readers can verify the list.
In `@internal/db/deployment_mapper_test.go`:
- Around line 168-177: Extend the “advance schedule” test case for
buildDeploymentMapperAdvanceSchedule with a negative SQL-fragment assertion,
following the existing “include archived omits archived filter” pattern. Verify
the generated statement does not contain any schedule_revision increment or
update, while preserving the existing positive fragments and argument
expectations.
In `@internal/db/deployment_mapper.xml`:
- Around line 224-233: Add a partial index in migration 00049 matching the
ListSchedulesMissingNextScheduledAt predicates, covering active, non-archived,
non-deleted deployments with a non-null schedule and null next_scheduled_at; add
the corresponding deployments_uninitialized_schedule_idx drop statement to the
migration’s Down section.
In `@internal/db/migrations/00049_schedule_deployments_with_river.sql`:
- Around line 13-31: Address the lock impact in migration 00049 by either
explicitly confirming that deployment_runs is small in every deployed
environment and keeping the current operations, or split the migration to use
NOT VALID followed by later VALIDATE CONSTRAINT and concurrent index creation
with the required goose NO TRANSACTION directive. Preserve the metadata-only
trigger_context removal, acknowledging its irreversible behavior.
In `@internal/db/webhooks.go`:
- Around line 70-98: Refactor the event processing loop around
ListActiveForEvent and hasEndpoints to cache active endpoints by event type,
using the actual row type returned by endpointMapper.ListActiveForEvent for the
map values. Reuse cached results for repeated event types so each type is
queried once, and move the loop-invariant fallback handling for !hasEndpoints
outside the endpoint lookup path while preserving existing payload and insertion
behavior.
In `@internal/deployments/cron_test.go`:
- Around line 9-72: Reorder the test functions so
TestNormalizeOptionalScheduleRejectsUnsupportedSyntax appears before the
successful nextScheduledTimes tests TestNextScheduledTimesHandlesLeapDay and
TestNextScheduledTimesHandlesDST. Keep each test’s implementation unchanged and
retain TestNormalizeOptionalScheduleAcceptsSundaySeven with the success
scenarios.
- Around line 54-65: Add the unsatisfiable expression `0 0 30 2 *` to the
`tests` table in `TestNormalizeOptionalScheduleRejectsUnsupportedSyntax`,
preserving the existing assertion that `normalizeOptionalSchedule` returns an
error for every listed expression. Ensure the parser and normalization logic
reject this expression before enabling the test.
In `@internal/deployments/cron.go`:
- Around line 68-72: Update the timezone validation in the schedule parsing flow
around config.Timezone and time.LoadLocation: either assign an explicit UTC
default when the trimmed value is blank, or reject blank values as invalid if
timezone selection is required. Ensure the chosen behavior is explicit and
consistent with the API contract before loading the location.
In `@internal/deployments/execution.go`:
- Around line 31-43: The prepareDeploymentRun error paths for
sessionEventsFromInitialEvents, sessionResourcesFromDeployment, and
httpapi.MarshalRaw currently return unclassified errors that scheduler.go
records as session_resource_not_found_error. Update prepareDeploymentRun to
return a classified error, or wrap each failure with an appropriate distinct
error type, so recordFailure and shouldAutoPause receive the correct failure
reason for malformed events, resource preparation, and marshal failures.
In `@internal/deployments/handler.go`:
- Around line 620-638: Move the deployment read and nextScheduledAt calculation
into the transaction callback used by the unpause flow, ensuring they operate on
the transaction’s current or locked row before calling UnpauseDeploymentTx and
enqueueScheduledOccurrenceTx. Keep error logging and HTTP error responses
consistent, and ensure the persisted next_scheduled_at and enqueued job derive
from the same schedule.
- Around line 775-862: Split validateRunReferences into the requested helpers:
validateAgentReferences, validateSkillReferences, validateVaultReferences, and
validateResourceReferences. Move each corresponding validation block into its
helper, preserving existing error classification, ordering, and return behavior,
then have validateRunReferences invoke them sequentially and continue only when
each succeeds.
- Around line 289-297: Rename the local result variable in create from
nextScheduledAt to nextRunAt so it does not shadow the nextScheduledAt function,
update its nil check and revision logic, and use nextRunAt in the
NextScheduledAt struct field assignment.
- Around line 822-861: Replace the sequential lookups in the vault and resources
validation flow with batch-by-external-ID queries for vaults, files, and memory
stores, grouping resource IDs by type before querying. Validate the returned
records as a set, preserving missing-reference and archived-record handling
through classifyReferenceFailure, and keep the existing invalid JSON behavior
unchanged.
In `@internal/deployments/scheduler_test.go`:
- Around line 1-9: Add a test in the scheduler test suite covering the
successful scheduled execution path: verify session, deployment run, and outbox
rows are created and the schedule cursor advances. Reuse the existing test
fixtures, helpers, and success-path symbols in the scheduler implementation,
while preserving the current failure, auto-pause, and webhook tests.
In `@internal/deployments/scheduler.go`:
- Around line 217-220: Update the invalid-schedule handling in Work around
nextAfterScheduled so an unparseable stored schedule is treated as a
non-retryable preparation failure: record a failure run and automatically pause
the deployment, matching the existing handling for other non-retryable
preparation failures. Ensure Work does not return the parse error for River
retry, and keep reconcile from repeatedly skipping the unchanged active
deployment.
- Around line 263-288: After a successful ApplyScheduledOccurrence call in the
scheduledDeploymentWorker flow, immediately enqueue the newly computed
nextScheduledAt occurrence instead of waiting for reconciliation. Update
scheduledDeploymentWorker or its dependencies to provide enqueueNext with the
River client or a minimal inserter interface, while preserving the reconcile
loop as the recovery path and existing error handling.
- Around line 82-94: The DeploymentScheduler.Start method currently blocks
startup and propagates per-deployment failures from backfillNextScheduledAt and
reconcile. Move the initial backfill/reconcile work into the background
reconciliation flow so client.Start executes without waiting, and handle
individual deployment errors by logging them while allowing the loop to continue
rather than returning them as startup errors.
In `@internal/webhooks/enqueuer_test.go`:
- Around line 16-37: Reorder the tests in the relevant test file so the existing
failure-scenario test using failingEnqueueStore appears before
TestPrepareDeliveryEventPreservesOutboxData. Do not change either test’s
implementation or assertions.
- Around line 22-36: Extend the assertions in the PrepareDeliveryEvent test to
verify event.ID, event.Data.Type, event.Data.WorkspaceID, and
event.Data.OrganizationID alongside the existing fields. Split the combined
condition into field-specific assertions so failures identify the mismatched
field, and add the strings import only if needed for the expected workspace or
organization value checks.
In `@main.go`:
- Around line 136-142: Increase the shutdown context timeout in the deferred
cleanup around deploymentScheduler.Stop from 10 seconds to a value longer than
the scheduler’s SoftStopTimeout, such as 20 seconds, so the hard-stop phase can
complete without recurring timeout errors.
In `@tests/deployments_api_test.go`:
- Around line 720-768: Reorder the subtests in the test suite so the failure
scenarios “failure auto pause rolls back...”, “failure scheduled root agent
archive...”, and “failure agent archive rolls back...” appear before the
successful “agent API archives deployments with webhook outbox” subtest. Keep
each subtest’s implementation unchanged.
In `@web/src/features/managed-agents/ManagedAgentsPage.resources.suite.tsx`:
- Line 2187: Update the fixture assignment near api.resources.deployments in the
managed agents resources suite to match the real DeploymentApiResponse schedule
shape by using the response’s expression field instead of
QuickstartDeploymentInput’s cron_expression; if this schedule is not exercised,
remove the assignment.
🪄 Autofix
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 468a4729-af4c-4a0c-9e8f-6cd499959c24
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (35)
cmd/migrate/main.godocs/design/be/deployments-api-contract.mdgo.modinternal/agents/handler.gointernal/api/server.gointernal/config/defaults.gointernal/db/agents.gointernal/db/db.gointernal/db/deployment_mapper.gointernal/db/deployment_mapper.xmlinternal/db/deployment_mapper_test.gointernal/db/deployment_run_mapper.gointernal/db/deployment_run_mapper.xmlinternal/db/deployments.gointernal/db/migrations/00049_schedule_deployments_with_river.sqlinternal/db/migrations_test.gointernal/db/webhooks.gointernal/deployments/cron.gointernal/deployments/cron_test.gointernal/deployments/execution.gointernal/deployments/handler.gointernal/deployments/handler_contract_test.gointernal/deployments/resources.gointernal/deployments/scheduler.gointernal/deployments/scheduler_test.gointernal/webhooks/enqueuer.gointernal/webhooks/enqueuer_test.gomain.gotests/deployments_api_test.gotests/uuid_boundary_postgres_test.goweb/src/features/managed-agents/ManagedAgentsPage.resources.suite.tsxweb/src/features/managed-agents/ManagedAgentsPage.test-utils.tsxweb/src/features/managed-agents/resources/detail.tsxweb/src/features/managed-agents/resources/model.tsxweb/src/features/managed-agents/types.ts
💤 Files with no reviewable changes (1)
- web/src/features/managed-agents/resources/model.tsx
| golang.org/x/tools v0.47.0 // indirect | ||
| google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect | ||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect | ||
| google.golang.org/grpc v1.80.0 // indirect |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Resolve the first patched grpc version and confirm the River release.
set -euo pipefail
gh api graphql -f query='
{
securityVulnerabilities(first: 10, ecosystem: GO, package: "google.golang.org/grpc") {
nodes {
advisory { ghsaId summary severity publishedAt }
vulnerableVersionRange
firstPatchedVersion { identifier }
}
}
}'
curl -s "https://proxy.golang.org/google.golang.org/grpc/@latest" | jq '.Version'
curl -s "https://proxy.golang.org/github.com/riverqueue/river/@v/v0.42.0.info" | jq '.'Repository: superduck-ai/open-managed-agents
Length of output: 2017
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== go.mod grpc entries =="
rg -n 'google\.golang\.org/grpc|google\.golang\.org/genproto|google\.golang\.org/protobuf|require \(' go.mod
echo
echo "== first 130 lines =="
sed -n '1,130p' go.mod
echo
echo "== downstream direct deps requiring grpc v1.80.0 =="
rg -n 'google\.golang\.org/grpc v1\.80\.0|google\.golang\.org/grpc \v1\.80\.0' .Repository: superduck-ai/open-managed-agents
Length of output: 4675
Upgrade google.golang.org/grpc to patch GHSA-hrxh-6v49-42gf.
v1.80.0 is vulnerable; add an explicit require or run go get for at least google.golang.org/grpc v1.82.1, then update go.sum.
🧰 Tools
🪛 OSV Scanner (2.4.0)
[HIGH] 84-84: google.golang.org/grpc 1.80.0: Vulnerabilities in the xDS RBAC authorization engine and the HTTP/2 transport server implementation in google.golang.org/grpc
(GO-2026-6061)
[HIGH] 84-84: google.golang.org/grpc 1.80.0: gRPC-Go: xDS RBAC and HTTP/2 Vulnerabilities
🤖 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 `@go.mod` at line 84, Upgrade the google.golang.org/grpc dependency from
v1.80.0 to at least v1.82.1 in go.mod, preserving it as an explicit requirement
if needed, and regenerate go.sum so the updated module checksums are recorded.
Source: Linters/SAST tools
| func (d *DB) SetInitialDeploymentNextScheduledAt(ctx context.Context, state DeploymentScheduleState, nextScheduledAt time.Time) error { | ||
| deploymentMapper := NewDeploymentMapper(d.mapperDB) | ||
| _, err := deploymentMapper.SetInitialNextScheduledAt(ctx, setInitialNextScheduledAtParams{ | ||
| WorkspaceUUID: state.WorkspaceUUID, ExternalID: state.ExternalID, | ||
| ScheduleRevision: state.ScheduleRevision, NextScheduledAt: nextScheduledAt, | ||
| }) | ||
| return err | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not discard the rows-affected count from the guarded update.
SetInitialNextScheduledAt is a conditional update. It matches zero rows when the schedule revision advanced, when the deployment was paused or archived, or when next_scheduled_at was already set by a concurrent reconciler. SetInitialDeploymentNextScheduledAt drops that count and returns nil, so the caller cannot distinguish "cursor persisted" from "nothing changed".
The caller then enqueues a River job for a cursor that was never stored. The worker rejects it as stale and the occurrence is skipped until the next reconciliation tick.
Return the outcome so the scheduler can skip the enqueue.
🛡️ Proposed fix
-func (d *DB) SetInitialDeploymentNextScheduledAt(ctx context.Context, state DeploymentScheduleState, nextScheduledAt time.Time) error {
+func (d *DB) SetInitialDeploymentNextScheduledAt(ctx context.Context, state DeploymentScheduleState, nextScheduledAt time.Time) error {
deploymentMapper := NewDeploymentMapper(d.mapperDB)
- _, err := deploymentMapper.SetInitialNextScheduledAt(ctx, setInitialNextScheduledAtParams{
+ rowsAffected, err := deploymentMapper.SetInitialNextScheduledAt(ctx, setInitialNextScheduledAtParams{
WorkspaceUUID: state.WorkspaceUUID, ExternalID: state.ExternalID,
ScheduleRevision: state.ScheduleRevision, NextScheduledAt: nextScheduledAt,
})
- return err
+ if err != nil {
+ return err
+ }
+ if rowsAffected != 1 {
+ return ErrStaleSchedule
+ }
+ return nil
}The scheduler must then treat ErrStaleSchedule as a skip, matching the handling already present in the worker.
🤖 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 `@internal/db/deployments.go` around lines 319 - 326, Update
SetInitialDeploymentNextScheduledAt to preserve the rows-affected result from
SetInitialNextScheduledAt and return ErrStaleSchedule when the guarded update
affects zero rows, while returning nil for a successful update and propagating
database errors. Ensure the scheduler handles ErrStaleSchedule by skipping the
River enqueue, consistent with the worker’s existing stale-schedule handling.
| update deployment_runs | ||
| set scheduled_at = (trigger_context ->> 'scheduled_at')::timestamptz | ||
| where trigger_type = 'schedule'; | ||
|
|
||
| alter table deployment_runs | ||
| add constraint deployment_runs_scheduled_at_check check ( | ||
| (trigger_type = 'schedule' and scheduled_at is not null) | ||
| or (trigger_type <> 'schedule' and scheduled_at is null) | ||
| ); | ||
|
|
||
| create unique index deployment_runs_schedule_occurrence_idx | ||
| on deployment_runs (deployment_uuid, scheduled_at) | ||
| where trigger_type = 'schedule'; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
The migration aborts if existing schedule rows have incomplete or duplicate trigger context.
Two failure modes exist on pre-existing data:
- A row with
trigger_type = 'schedule'whosetrigger_contextlacks ascheduled_atkey backfills toNULL. Thedeployment_runs_scheduled_at_checkconstraint then rejects it and the migration aborts. - Two rows with the same
deployment_uuidand the same backfilledscheduled_atbreak the new unique indexdeployment_runs_schedule_occurrence_idx.
Failing is better than silently writing NULL, and it matches the repository rule for reference backfills. But the failure message is opaque. Verify the current data first, and consider a pre-check that raises an explicit error.
As per coding guidelines: "将既有 bigint 引用迁移为 UUID 时,必须先通过源表回填并验证每一条引用均可解析;发现孤立引用时必须让 migration 失败,不能静默写成 NULL" — the same verify-then-fail principle applies to this backfill.
🛡️ Proposed explicit pre-check
update deployment_runs
set scheduled_at = (trigger_context ->> 'scheduled_at')::timestamptz
where trigger_type = 'schedule';
+-- +goose StatementBegin
+do $$
+begin
+ if exists (select 1 from deployment_runs where trigger_type = 'schedule' and scheduled_at is null) then
+ raise exception 'deployment_runs rows with trigger_type = schedule are missing trigger_context.scheduled_at';
+ end if;
+ if exists (
+ select 1 from deployment_runs where trigger_type = 'schedule'
+ group by deployment_uuid, scheduled_at having count(*) > 1
+ ) then
+ raise exception 'duplicate scheduled occurrences exist for the same deployment_uuid and scheduled_at';
+ end if;
+end
+$$;
+-- +goose StatementEnd
+
alter table deployment_runs📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| update deployment_runs | |
| set scheduled_at = (trigger_context ->> 'scheduled_at')::timestamptz | |
| where trigger_type = 'schedule'; | |
| alter table deployment_runs | |
| add constraint deployment_runs_scheduled_at_check check ( | |
| (trigger_type = 'schedule' and scheduled_at is not null) | |
| or (trigger_type <> 'schedule' and scheduled_at is null) | |
| ); | |
| create unique index deployment_runs_schedule_occurrence_idx | |
| on deployment_runs (deployment_uuid, scheduled_at) | |
| where trigger_type = 'schedule'; | |
| update deployment_runs | |
| set scheduled_at = (trigger_context ->> 'scheduled_at')::timestamptz | |
| where trigger_type = 'schedule'; | |
| -- +goose StatementBegin | |
| do $$ | |
| begin | |
| if exists (select 1 from deployment_runs where trigger_type = 'schedule' and scheduled_at is null) then | |
| raise exception 'deployment_runs rows with trigger_type = schedule are missing trigger_context.scheduled_at'; | |
| end if; | |
| if exists ( | |
| select 1 from deployment_runs where trigger_type = 'schedule' | |
| group by deployment_uuid, scheduled_at having count(*) > 1 | |
| ) then | |
| raise exception 'duplicate scheduled occurrences exist for the same deployment_uuid and scheduled_at'; | |
| end if; | |
| end | |
| $$; | |
| -- +goose StatementEnd | |
| alter table deployment_runs | |
| add constraint deployment_runs_scheduled_at_check check ( | |
| (trigger_type = 'schedule' and scheduled_at is not null) | |
| or (trigger_type <> 'schedule' and scheduled_at is null) | |
| ); | |
| create unique index deployment_runs_schedule_occurrence_idx | |
| on deployment_runs (deployment_uuid, scheduled_at) | |
| where trigger_type = 'schedule'; |
🧰 Tools
🪛 Squawk (2.61.0)
[warning] 14-17: By default new constraints require a table scan and block writes to the table while that scan occurs. Use NOT VALID with a later VALIDATE CONSTRAINT call.
(constraint-missing-not-valid)
[warning] 19-21: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.
(require-concurrent-index-creation)
🤖 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 `@internal/db/migrations/00049_schedule_deployments_with_river.sql` around
lines 9 - 21, Before updating deployment_runs, add explicit validation for
existing schedule rows in the migration: fail with a clear error when
trigger_context.scheduled_at is missing or invalid, and when duplicate
deployment_uuid/scheduled_at occurrences would violate the new unique index.
Only perform the backfill and add deployment_runs_scheduled_at_check and
deployment_runs_schedule_occurrence_idx after these validations pass.
Source: Coding guidelines
| compactEntityId(run.id), | ||
| deploymentRunStatus(run), | ||
| run.trigger_type || triggerLabel(run.trigger), | ||
| run.trigger_context.type, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Guard the trigger context access and confirm the dropped label formatting.
listDeploymentRuns returns the API payload without runtime validation. If trigger_context is absent in a response, run.trigger_context.type throws and the whole runs table fails to render. The previous expression tolerated missing fields.
This change also renders the raw enum value. The removed triggerLabel helper produced a display label. Confirm that showing manual and schedule verbatim is the intended copy.
🛡️ Proposed guard
- run.trigger_context.type,
+ run.trigger_context?.type ?? '—',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| run.trigger_context.type, | |
| run.trigger_context?.type ?? '—', |
🤖 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 `@web/src/features/managed-agents/resources/detail.tsx` at line 680, Guard the
trigger context access in the runs table so missing trigger_context does not
throw and preserves the prior fallback behavior. Restore or reuse triggerLabel
for user-facing formatting instead of rendering raw enum values, unless the
intended copy explicitly requires manual and schedule.
|
这轮只修了 4 个正确性问题:Workspace 归档、无解 Cron、unpause 旧快照和执行配置 revision。30 秒 reconcile 和 Worker 全链路测试没动,按之前结论另算。 麻烦再看一轮。 @codex review |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
tests/deployments_api_test.go (1)
557-568: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake the lock-contention assertions deterministic.
The tests use elapsed time without proving that the competing operation reached the database lock boundary. A delayed goroutine can make either test pass without testing the required blocking behavior.
tests/deployments_api_test.go#L557-L568: Synchronize afterUpdateDeploymentreaches its row-lock attempt before committingworkerTx.tests/deployments_api_test.go#L658-L680: Signal after the unpause request reaches its row-lock attempt, not beforeapp.client.Do(req).Use a test hook or observable database lock state. Do not use a fixed sleep as the synchronization condition.
🤖 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 `@tests/deployments_api_test.go` around lines 557 - 568, Make the lock-contention assertions deterministic in tests/deployments_api_test.go at lines 557-568 and 658-680: add a test hook or observable database lock-state signal so the worker synchronizes only after UpdateDeployment reaches its row-lock attempt before committing workerTx, and the unpause test signals only after app.client.Do(req) reaches its row-lock attempt. Replace the fixed time-based synchronization with these signals; do not use sleeps or elapsed-time checks.docs/design/be/deployments-api-contract.md (1)
95-95: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win定义
deployment_runwebhook 的顺序和幂等处理规则。
succeeded/failed与started是当前独立jobs记录。订单号相同的jobs只能按run_after/created_at排序领取,当前实现不会按 Run 串行投递;重试或并发交付 may 导致终态事件先到达或重复送达。在合同中补充顺序键、重复记录规则,或实现/约束 Run 级别的 webhook delivery worker 串行性。🤖 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 `@docs/design/be/deployments-api-contract.md` at line 95, 补充 deployment_run webhook 的顺序与幂等契约:明确以同一 Run ID 作为顺序键,started 必须先于 succeeded/failed 投递,并规定重试或并发导致的重复事件及终态先到达时的处理规则;若无法仅通过合同保证,则约束对应 webhook delivery worker 按 Run 串行投递。internal/db/deployment_mapper.xml (1)
23-24: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winRun the Yourbatis generator for
DeploymentMapper.
internal/db/deployment_mapper.godeclaresdeployment_mapper.sqlmap.gen.goas generator output, and the XML now addsschedule_revision,next_scheduled_at, and the schedule-related statements. Regenerate matching generated code rather than shipping stale SQL-map output.🤖 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 `@internal/db/deployment_mapper.xml` around lines 23 - 24, Regenerate the MyBatis output for DeploymentMapper so deployment_mapper.sqlmap.gen.go reflects the XML additions, including schedule_revision, next_scheduled_at, and the schedule-related statements. Do not manually patch the generated file; run the repository’s established generator and include the resulting synchronized output.Source: Coding guidelines
🧹 Nitpick comments (1)
internal/db/deployment_mapper.xml (1)
224-243: 🚀 Performance & Scalability | 🔵 TrivialBound the reconciliation reads before scale-up.
Both queries return all matching deployments without a
LIMIT. With a 30-second reconciliation loop, each cycle can materialize the full active schedule set. Add keyset pagination or bounded batches, or validate that the expected schedule count and indexes keep this cost acceptable.This follows the PR objective of a 30-second reconciliation loop.
🤖 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 `@internal/db/deployment_mapper.xml` around lines 224 - 243, The ListActiveSchedules and ListSchedulesMissingNextScheduledAt queries currently read unbounded result sets each reconciliation cycle. Add bounded batching with keyset pagination using stable ordering keys, or otherwise enforce an appropriate LIMIT and continuation mechanism, while preserving their existing filters and ordering so reconciliation remains scalable.
🤖 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 `@docs/design/be/deployments-api-contract.md`:
- Line 65: 明确文档中的终态 Run 规则:每个 scheduled occurrence 都必须创建对应的 deployment_run,并为其生成
Run ID 和 webhook;创建 Session、Deployment Run 与 outbox 必须保持原子性。更新游标规则,使提交任一终态
Run(成功或非自动暂停的最终失败)后都推进到下一个 occurrence,仅自动暂停的失败停止推进。
---
Outside diff comments:
In `@docs/design/be/deployments-api-contract.md`:
- Line 95: 补充 deployment_run webhook 的顺序与幂等契约:明确以同一 Run ID 作为顺序键,started 必须先于
succeeded/failed 投递,并规定重试或并发导致的重复事件及终态先到达时的处理规则;若无法仅通过合同保证,则约束对应 webhook
delivery worker 按 Run 串行投递。
In `@internal/db/deployment_mapper.xml`:
- Around line 23-24: Regenerate the MyBatis output for DeploymentMapper so
deployment_mapper.sqlmap.gen.go reflects the XML additions, including
schedule_revision, next_scheduled_at, and the schedule-related statements. Do
not manually patch the generated file; run the repository’s established
generator and include the resulting synchronized output.
In `@tests/deployments_api_test.go`:
- Around line 557-568: Make the lock-contention assertions deterministic in
tests/deployments_api_test.go at lines 557-568 and 658-680: add a test hook or
observable database lock-state signal so the worker synchronizes only after
UpdateDeployment reaches its row-lock attempt before committing workerTx, and
the unpause test signals only after app.client.Do(req) reaches its row-lock
attempt. Replace the fixed time-based synchronization with these signals; do not
use sleeps or elapsed-time checks.
---
Nitpick comments:
In `@internal/db/deployment_mapper.xml`:
- Around line 224-243: The ListActiveSchedules and
ListSchedulesMissingNextScheduledAt queries currently read unbounded result sets
each reconciliation cycle. Add bounded batching with keyset pagination using
stable ordering keys, or otherwise enforce an appropriate LIMIT and continuation
mechanism, while preserving their existing filters and ordering so
reconciliation remains scalable.
🪄 Autofix
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 087e8caa-7ffe-48d3-bea5-f1ab2dac9ca2
📒 Files selected for processing (11)
docs/design/be/deployments-api-contract.mdinternal/db/db.gointernal/db/deployment_mapper.gointernal/db/deployment_mapper.xmlinternal/db/deployment_mapper_test.gointernal/db/deployments.gointernal/deployments/cron.gointernal/deployments/cron_test.gointernal/deployments/handler.gointernal/deployments/scheduler.gotests/deployments_api_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
- internal/db/db.go
- internal/db/deployment_mapper.go
- internal/db/deployment_mapper_test.go
- internal/deployments/scheduler.go
- internal/deployments/cron.go
- internal/db/deployments.go
- internal/deployments/handler.go
| - spring-forward 不存在的墙上时刻不触发;fall-back 重复的墙上时刻触发两次。 | ||
| - 实际 River Job 只正向延后。jitter 窗口为相邻名义 occurrence 间隔的 15%,下限 5 秒、上限 9 分钟;窗口内 offset 由 Deployment ID 与名义时刻的稳定哈希决定。这是 OMA 内部选择,不是 Claude 公开的哈希算法。 | ||
|
|
||
| 每个 Deployment 持久化 `schedule_revision` 和 `next_scheduled_at`。这个 revision 同时保护 schedule 和执行输入:create、明确修改或清除 schedule、修改 agent/environment/metadata/initial events/resources/vaults、pause、unpause、archive 都使旧 Job 失效;不影响执行的 PATCH 不改 revision。执行输入 PATCH 会保留事务内锁定行的 cursor,并为新 revision 重新入队,避免 worker 使用旧配置提交 Session。unpause 在锁定 Deployment 后计算下一个 occurrence,只从当前时间之后恢复,不补暂停期间的触发。worker 成功提交一个 Run 后推进到下一个名义 occurrence。create、相关 PATCH 和 unpause 通过 Yourbatis 公开的 `SQLTx()` 将同一个事务交给 River `InsertTx`,使游标与 Job 一起提交或回滚。worker 推进游标和启动回填后的 Job 仍由每 30 秒一次的 reconciliation 补齐,入队使用 `ByArgs` 保持幂等。启动回填或 reconciliation 遇到单条确定性的存量 schedule 解析错误时记录并跳过该 Deployment;数据库或 River 基础设施错误仍使启动失败。 |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
明确所有终态 Run 的创建和游标推进规则。
Line 79 的“Session 或失败 Run”没有明确创建成功的 deployment_run。Line 83 和 Line 95 要求每个 scheduled Run 具有对应的 Run ID 和 webhook;PR objective 也要求原子创建 Session、Deployment Run 和 outbox。
Line 65 的“成功提交一个 Run 后推进”也容易漏掉不暂停的最终失败。应在提交任一终态 Run 后推进游标;只有自动暂停的失败才停止推进。
建议修改
- worker 成功提交一个 Run 后推进到下一个名义 occurrence。
+ worker 成功提交任一终态 Run(succeeded 或 failed)后,未暂停时推进到下一个名义 occurrence。
- Worker->>AppDB: 原子写 Session 或失败 Run、推进/暂停游标并写 webhook outbox
+ Worker->>AppDB: 原子创建 Session 与 succeeded Run,或创建 failed Run;推进/暂停游标并写 webhook outboxAlso applies to: 78-79
🧰 Tools
🪛 LanguageTool
[uncategorized] ~65-~65: 您的意思是“"不"暂停”?
Context: ...eployment 后计算下一个 occurrence,只从当前时间之后恢复,不补暂停期间的触发。worker 成功提交一个 Run 后推进到下一个名义 occu...
(BU)
[uncategorized] ~65-~65: 您的意思是“"不"齐”?
Context: ...标和启动回填后的 Job 仍由每 30 秒一次的 reconciliation 补齐,入队使用 ByArgs 保持幂等。启动回填或 reconciliatio...
(BU)
🤖 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 `@docs/design/be/deployments-api-contract.md` at line 65, 明确文档中的终态 Run 规则:每个
scheduled occurrence 都必须创建对应的 deployment_run,并为其生成 Run ID 和 webhook;创建
Session、Deployment Run 与 outbox 必须保持原子性。更新游标规则,使提交任一终态
Run(成功或非自动暂停的最终失败)后都推进到下一个 occurrence,仅自动暂停的失败停止推进。
|
@codex review |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@internal/db/migrations/00050_schedule_deployments_with_river.sql`:
- Around line 14-28: Split the constraint and index changes into subsequent
numbered migrations: add deployment_runs_scheduled_at_check as NOT VALID, then
validate it in a later migration; create deployment_runs_schedule_occurrence_idx
and deployments_pending_schedule_idx with CREATE INDEX CONCURRENTLY in NO
TRANSACTION migrations. Update each Down path to drop the corresponding indexes,
preserving the existing constraint semantics.
🪄 Autofix
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b95686de-1cbb-42bd-bd64-4adc46f73f4a
📒 Files selected for processing (11)
internal/api/server.gointernal/config/reference_test.gointernal/db/agents.gointernal/db/db.gointernal/db/deployments.gointernal/db/migrations/00050_schedule_deployments_with_river.sqlinternal/db/migrations_test.gointernal/db/webhooks.gomain.gotests/uuid_boundary_postgres_test.goweb/src/features/managed-agents/resources/model.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
- web/src/features/managed-agents/resources/model.tsx
- internal/db/db.go
- internal/db/migrations_test.go
- main.go
- internal/db/agents.go
- internal/api/server.go
- tests/uuid_boundary_postgres_test.go
- internal/db/webhooks.go
- internal/db/deployments.go
| add constraint deployment_runs_scheduled_at_check check ( | ||
| (trigger_type = 'schedule' and scheduled_at is not null) | ||
| or (trigger_type <> 'schedule' and scheduled_at is null) | ||
| ); | ||
|
|
||
| create unique index deployment_runs_schedule_occurrence_idx | ||
| on deployment_runs (deployment_uuid, scheduled_at) | ||
| where trigger_type = 'schedule'; | ||
|
|
||
| create index deployments_pending_schedule_idx | ||
| on deployments (next_scheduled_at) | ||
| where status = 'active' | ||
| and archived_at is null | ||
| and deleted_at is null | ||
| and next_scheduled_at is not null; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Verify Goose transaction conventions and existing concurrent-index migrations.
rg -n -C 3 --glob '*.sql' --glob '*.go' \
'NO TRANSACTION|CREATE( UNIQUE)? INDEX CONCURRENTLY|DROP INDEX CONCURRENTLY|VALIDATE CONSTRAINT|goose\.' \
internal cmd/migrate/main.go main.go || trueRepository: superduck-ai/open-managed-agents
Length of output: 2852
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo '--- target migration ---'
cat -n internal/db/migrations/00050_schedule_deployments_with_river.sql
echo '--- deployment_runs/deployments schema references ---'
rg -n -C 4 'deployment_runs|deployments|scheduled_at|next_scheduled_at|trigger_type|check \(' internal/db/migrations internal/db --glob '*.sql'
echo '--- similar validate-constraint/concurrent migrations ---'
python3 - <<'PY'
from pathlib import Path
import re
for p in sorted(Path('internal/db/migrations').glob('*.sql')):
txt=p.read_text()
if re.search(r'CREATE\s+(UNIQUE\s+)?INDEX\s+.*CONCURRENTLY|DROP\s+INDEX\s+CONCURRENTLY|ADD\s+CONSTRAINT\s+.*NOT\s+VALID|VALIDATE\s+CONSTRAINT', txt, re.I):
print(p)
print(txt.splitlines()[0])
PYRepository: superduck-ai/open-managed-agents
Length of output: 50389
🌐 Web query:
PostgreSQL CREATE INDEX CONCURRENTLY invalidates table writes indexes are built concurrently documentation
💡 Result:
No, the PostgreSQL CREATE INDEX CONCURRENTLY command does not invalidate or block table writes [1][2]. Unlike a standard index build, which acquires a lock that prevents concurrent INSERT, UPDATE, and DELETE operations on the table, CREATE INDEX CONCURRENTLY is specifically designed to allow normal DML operations (reads and writes) to continue while the index is being built [1][2][3]. Key characteristics of this process include: 1. Lock Mode: It uses a ShareUpdateExclusiveLock, which allows concurrent data modifications (inserts, updates, and deletes) while blocking only other schema-changing operations (such as ALTER TABLE or VACUUM FULL) [2][3]. 2. Multi-phase Operation: The process involves multiple table scans [1][2]. During these phases, the system ensures that changes made to the table by other transactions are correctly accounted for in the new index [2]. 3. Invalid Index State: If the command is interrupted or fails before completion, it may leave an "invalid" index behind [2][3]. Such an index does not support queries and must be manually dropped [2][3]. 4. Performance and Duration: Because the operation does not block writes, it requires more total work and takes significantly longer to complete than a standard index build [1][4]. It also consumes more CPU and I/O resources during this extended period [1][2]. 5. Finalization: At the very end of the process, the command acquires a brief AccessExclusiveLock to finalize the index status in the system catalogs, but this hold is typically very short [3]. In summary, CREATE INDEX CONCURRENTLY is the standard approach for creating indexes on live production tables without disrupting write activity, provided you can accommodate the longer build time and higher resource overhead [1][3].
Citations:
- 1: https://www.postgresql.org/docs/current/sql-createindex.html
- 2: https://www.bytebase.com/blog/postgres-create-index-concurrently/
- 3: https://alexstoica.com/blog/create-index-concurrently-locks
- 4: https://www.postgresql.org/docs/15/sql-createindex.html
Avoid blocking table writes during migration.
The new check constraint validates rows immediately, and both indexes use normal CREATE INDEX. On live data this can stall deployment and deployment run traffic during these statements. Split these changes into new numbered migrations: add the constraint with NOT VALID; validate deployment_runs_scheduled_at_check in a later migration; create both indexes with CREATE INDEX CONCURRENTLY in NO TRANSACTION migrations; drop the normal indexes in the Down path.
🧰 Tools
🪛 Squawk (2.61.0)
[warning] 14-17: By default new constraints require a table scan and block writes to the table while that scan occurs. Use NOT VALID with a later VALIDATE CONSTRAINT call.
(constraint-missing-not-valid)
[warning] 19-21: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.
(require-concurrent-index-creation)
[warning] 23-28: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.
(require-concurrent-index-creation)
🤖 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 `@internal/db/migrations/00050_schedule_deployments_with_river.sql` around
lines 14 - 28, Split the constraint and index changes into subsequent numbered
migrations: add deployment_runs_scheduled_at_check as NOT VALID, then validate
it in a later migration; create deployment_runs_schedule_occurrence_idx and
deployments_pending_schedule_idx with CREATE INDEX CONCURRENTLY in NO
TRANSACTION migrations. Update each Down path to drop the corresponding indexes,
preserving the existing constraint semantics.
Sources: Coding guidelines, Linters/SAST tools
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dade2a3a66
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| now := time.Now().UTC() | ||
| referenceFailure, err := validateRunReferences(ctx, w.database, deployment.WorkspaceUUID, deployment) |
There was a problem hiding this comment.
Recheck environment state in the occurrence transaction
When an environment is archived after this preflight read but before ApplyScheduledOccurrence acquires its locks, the archive only updates the environment row and does not invalidate the deployment revision. The final transaction locks only the deployment and workspace, while insertSessionTx inserts the queued environment work without checking Environment.ArchivedAt; the runner likewise loads and executes that archived environment. Move the environment-state check/lock into the occurrence transaction or make environment archival atomically invalidate affected schedules.
AGENTS.md reference: AGENTS.md:L114-L116
Useful? React with 👍 / 👎.
| config.Timezone = strings.TrimSpace(config.Timezone) | ||
| location, err := time.LoadLocation(config.Timezone) |
There was a problem hiding this comment.
Reject an omitted schedule timezone
When timezone is missing or whitespace-only, trimming produces "", and Go's time.LoadLocation("") succeeds by returning UTC. Such requests are therefore accepted and persisted with an empty timezone even though the API previously required a non-empty timezone and the schedule contract requires a valid IANA name; add an explicit empty-string rejection before loading the location.
AGENTS.md reference: AGENTS.md:L114-L116
Useful? React with 👍 / 👎.


背景与目标
Deployment API 已经能够保存
schedule并返回upcoming_runs_at,但此前没有持久化调度器真正执行这些 occurrence。服务重启、并发实例、任务重试以及 API 写入与调度入队之间的失败,都可能让只依赖进程内定时器的方案失去执行状态。这个 PR 要解决的是:让 scheduled Deployment 能够可靠地创建 Session 和 Deployment Run,并把调度游标、幂等边界、自动暂停和 webhook 事件纳入同一套可恢复的事务模型。
主要改动
v0.42.0作为 PostgreSQL 持久任务队列,注册独立的deployment_schedulesqueue 和 scheduled Deployment worker。main自动迁移和cmd/migrate up都会依次推进两套 migration。upcoming_runs_at保持名义时刻,不包含 jitter。schedule_revision和next_scheduled_at,以 revision + occurrence 使旧 Job 失效并推进调度游标。InsertTx,保证 Deployment 状态与首个 Job 一起提交或回滚。(deployment_uuid, scheduled_at)的部分唯一索引作为 River at-least-once 投递下的最终幂等边界。trigger_context、Deployment/Deployment Run webhook、Agent 归档级联 Deployment,以及前端 Run 列表展示。为什么引入 River
这里需要的不是单纯的 Cron 计算,而是持久化执行生命周期:Job 必须跨进程重启保留,失败可以重试,多实例可以竞争消费,并且首次入队需要和业务状态共用 PostgreSQL 事务。River 提供了这些能力,同时允许应用保留自己的调度游标和最终幂等约束。
当前没有把 Deployment 直接建模为 River 周期任务。每个名义 occurrence 对应一个带
schedule_revision + scheduled_at的一次性 Job;Deployment 表是 schedule 状态的事实来源,River 负责可靠投递,应用唯一索引负责最终去重。这样 pause、unpause、修改 schedule 和 archive 都可以通过游标和 revision 原子仲裁。flowchart LR API["Create / PATCH / Unpause API"] --> TX["Yourbatis transaction"] TX --> DEP["Deployment<br/>revision + cursor"] TX -->|"River InsertTx"| JOB["River Job"] JOB -->|"scheduled_at + deterministic jitter"| WORKER["Scheduled worker"] WORKER --> CHECK["校验 active / revision / cursor"] CHECK --> OCC["Occurrence transaction"] OCC --> SESSION["Session"] OCC --> RUN["Deployment Run"] OCC --> OUTBOX["Webhook outbox"] OCC --> NEXT["推进 cursor / 自动暂停"] NEXT -->|"30s reconcile"| JOBworker 完成后不立即插入下一跳,而由每 30 秒运行一次的 reconciliation 根据持久化 cursor 补齐 Job。这是有意选择的最终调度模型:不会丢失 occurrence,重启后也能恢复,但高频 Cron 可能增加几秒到几十秒延迟。如果产品后续要求秒级贴近名义时刻,再单独评估成功事务内直接 enqueue 下一跳。
事务、失败与幂等设计
ByArgs去重;参数包含 workspace、Deployment、schedule revision 和名义 scheduled time。schedule_revision + next_scheduled_at仲裁过期或重复 Job。deployment.archivedoutbox。文档与合同对齐
本 PR 更新了
docs/design/be/deployments-api-contract.md,记录 River migration、Cron/DST/jitter、cursor/revision、reconciliation、scheduled Run、自动暂停和 webhook 事务语义。实现同时对齐或参考:
docs/design/be/yourbatis-guidelines.md:应用 SQL、事务和 Mapper 继续通过 Yourbatis;仅 River 官方 migrator 管理第三方内部 schema。验证
just test(包含 PostgreSQL 集成测试)just lintjust dead-codejust complexityjust duplicatesjust large-filesjust hooks-runjust web-format-checkjust web-lint-namingjust web-test(416 tests passed)just web-buildgit diff --cached --checkgit diff --checkReview focus / 已知待确认项
这是 Draft PR,当前希望同事重点确认以下边界,再决定是否 Ready for merge:
schedule_revision。需要确认是否应让调度 token 覆盖全部执行输入,避免并发 PATCH 时 Session 使用旧配置、Run 对应新 Deployment 的快照竞态。robfig/cron对永远不会发生的表达式可能返回零时间,例如0 0 31 2 *;当前需要补IsZero()防守。timezone 缺失/空字符串以及 Sunday7带非法 step 的验证也请一并确认。scheduledDeploymentWorker.Work()的完整成功路径。建议至少补一个 worker 集成测试;是否要求完整的 HTTP → River → Work 链路可由 review 决定。Summary by CodeRabbit
New Features
Documentation