Repository navigation
perf: scale agent execution with typed execution state, a claim dispatcher, and owned continuation - #631
perf: scale agent execution with typed execution state, a claim dispatcher, and owned continuation#631ksarangmath wants to merge 13 commits into
Conversation
…tcher, and owned continuation Replace history-derived scheduling with an agent execution engine. Each agent has a typed execution head of pointers, monotonic boundaries and summaries recomputed at commit, plus immutable opening lineage on model contexts, config changes and checkpoints. One pure selector decides next work for claim, owned continuation, release and recovery. Semantic transitions run through a cell-bound unit of work that plans locks up front in sorted order, supports savepoints, and publishes each changed agent's head and queue in two pipelined statements before enqueueing webhooks and post-commit notifications. Execution writes live in a private query package enforced by SQL rules and a Go analyzer. Workers run a bounded claim dispatcher, keep an owned agent across ready steps within a configurable budget, and accept model output, select and prepare the next call in one transaction. Migration 052 builds every existing agent's execution state from history, validates it, and drops the history frontier functions. It requires a writer-stop maintenance window. Also fixes: - continuations stranded when a turn carried unanswered inputs from an earlier turn - child-before-parent and machine/parent lock-order hazards - subagent launch reusing a machine grant deleted while it waited - project and organization deletion skipping event webhooks Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
6b2681e to
a73cced
Compare
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep each handle's execution view in memory across selections within a unit, updating it from successful receive, admission, preparation and output writes and invalidating it otherwise, so publication reloads only when a selection is stale. Move webhook eligibility capture into publication, simplify the facts query, validate single-input opening lineage without set operations, plan parent and root routing in one read, and reuse the claim's root identity. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Check the continuation budget only before preparing the next step, so a prepared step always runs. - Accept model output before optional advancement. Advance inside a savepoint; on failure, commit acceptance alone and surface the cause to the kernel, which logs it. - Archive cancels in-flight work before marking the agent archived, and facts load independently of lifecycle state. - Derive organization-deletion and grant-revocation lock sets from the rows they clean up. A unit that needs an agent outside its plan rolls back and restarts with the expanded sorted plan, and units can only be opened inside that retry boundary. - Lock only the target agent for ordinary transitions so siblings do not serialize on their parent. - Enforce opening-lineage membership and order with triggers. - Replace the discriminated fact UNION with typed queries in one batch. - Remove the unused answered boundary, cell identity and authority tags. - Suppress webhooks for deleted projects and organizations and document it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 170dead. Configure here.
|
|
||
| -- name: ExecutionModelRevision :one | ||
| -- @sqlc-vet-disable configured-models-deleted-at | ||
| -- @sqlc-vet-disable model-provider-configs-deleted-at |
There was a problem hiding this comment.
sqlc-vet-disable lacks rationale comments
Low Severity
New @sqlc-vet-disable annotations on ExecutionModelRevision, ListExecutionIntegrationTargetAgents, and ClearExecutionIntegrationTargets omit an adjacent rationale for which vet rule is disabled and why that is safe. Existing disables on main always include that comment.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 170dead. Configure here.
… claim - Run fused acceptance and advancement in one transaction. On a non-plan failure, roll back and accept alone in a fresh unit, as the commit-failure path already did. This removes the subtransaction that turned released lease rows into MultiXacts, saves two statements per fused step, and deletes the unit savepoint and snapshot machinery. - Stop anti-joining every runtime lease when choosing the next claim. Leased agents have no wakeup row, so the claim walks the ready index and stops at the first agent it can lock, with a single-row lease recheck. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fold into the 052 cutover: - Index tool_calls.runtime_lock_id so the foreign-key check on every lease delete no longer scans all tool-call history. - Index received content inputs per agent for the execution backlog. - Replace the org and project references on model contexts, processes and process actions with one org-qualified reference to the agent row, which the writer already holds locked, and drop the redundant project reference on agent inputs. Inserts stop key-share locking the shared org and project rows. Config and model-revision references stay. - Move the payload-time helper from 053 into 052, so the cutover is a single migration. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Move a queued input by reading it and its anchor, finding the destination neighbor with one indexed lookup, and updating one row. When a rank gap is exhausted, renumber that agent's pending inputs in the mode with a single statement and retry once. - Rank new content inputs from the last pending input in their mode; config changes skip ranking. - Admit only the selected queued input or pending steering inputs, with the remaining-input flags in the same statement. - Serve every engine backlog read from one partial index over pending content inputs, replacing the two project-scoped per-mode indexes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Reject a new content submission with agent_backlog_full (HTTP 409) when the agent already has 50 pending inputs, counting queued and steering together. The check runs under the agent lock after the idempotency lookup, so replays still succeed, and counts at most 50 index entries. - Accept a child's results, failures and questions to its parent past the cap; they still count toward it. - Leave integration inbox deliveries pending for retry, record a skipped cron occurrence, and fail a parent's message to a full subagent with a tool error the parent can read. - Show the error in the composer and restore the exact unsent draft without discarding text typed while the send was in flight. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
This PR is too large for Bugbot to review. It changes 59,580 lines and 3,060,629 characters. Split the change into smaller pull requests to get a review. |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
…ibility Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Upload inline attachments as prepared blobs, then insert their artifact rows inside the admission transaction, after the idempotency and capacity checks. A rejected submission leaves no artifact rows. - Delete prepared blobs after a definite rejection or an idempotent replay, and keep them when the commit outcome is unknown. - Match idempotent replays by attachment content type, filename, digest and size. - Tell integration senders the agent's queue is full when deliveries exhaust their retries on a full backlog. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A retry with a known idempotency key returns the stored input or config change without comparing the request. This removes the field and content comparisons, the attachment replay matching and the actor lookup they needed, so a retry after promoting a queued input to steering returns the original instead of conflicting. A config-change replay now returns the stored config, input and event before preparing a new config. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Roots whose transition will report to the parent now plan the agent family before writing: a model output that ends the turn without tool calls, a terminal model failure and a terminal compaction failure. These complete in one attempt instead of rolling back to add the parent. Outputs that call tools still lock only the child. AcceptOutput and the planning use one shared rule for which outputs report to the parent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>


Summary
Under sustained load, agent execution was limited by the harness rather than by model latency or Postgres capacity. A load test running the real API, worker and maintenance binaries against Postgres 18, with a fake streaming model, showed four costs:
This PR fixes all four. It replaces history-derived scheduling with an agent execution engine that keeps typed per-agent state and updates it through explicit transitions.
Worker
OMNARA_WORKER_CLAIM_CONCURRENCY, default 4). A loop reserves an execution slot before claiming, so a worker never leases more agents than it can run.OMNARA_WORKER_CONTINUATION_MAX_MODEL_STARTS(default 2 additional model calls) andOMNARA_WORKER_CONTINUATION_MAX_DURATION(default 30s).Agent execution engine (
internal/storage/internal/agentexecution)Typed execution state.
agent_execution_stateholds one row per agent:It has no counters and no JSON. A model context, config change or checkpoint records the inputs that opened its turn once, as an immutable typed array. Database triggers check that every member is an admitted input of the same agent, in event order.
One selection policy. A pure
SelectNextfunction decides the next work. Global claim, owned continuation, release and recovery all use it. It loads state in two round trips: the agent head, then a pipelined batch of typed queries for the current turn and the rows its pointers reference.Semantic transitions. Each transition is an explicit operation:
Unit of work. Every execution-writing transaction runs through one unit. A unit can only be opened inside a retry boundary. The unit:
Ordinary transitions lock only their own agent, so sibling subagents don't serialize on their parent.
Backlog. Moving, ranking and admitting queued inputs use indexed lookups on one partial index over pending content inputs. A move updates one row. When a rank gap runs out, one statement renumbers that agent's pending inputs in that mode (74 ms for 10,000 pending inputs).
Enforced boundary. Execution writes live in a private query package. SQL rules and a Go analyzer reject execution DML anywhere else.
Removed. The 14 history frontier functions, the inferred-turn views, the scattered wakeup reconciliation, and wakeup metadata columns.
Fixes found along the way
tool_calls.runtime_lock_idwas partial on running calls, so the foreign-key check on each lease delete was a sequential scan of all tool-call history: 5.8 ms at 100,000 tool calls. A partial index on the reference makes it 0.1 ms regardless of history.Results
Measured at e2ab1ef, before the schema changes in 3ad8a45 (measured with the plan tests above, since the load test starts from a fresh database every run), on a fanless MacBook Air (Apple M4, 4 performance and 6 efficiency cores, 24 GB) running every component:
Before each run, the Go build cache, database and Redis are reset, and the machine idles back to its idle temperature. Runs alternate between
mainand this branch.mainWith no idle time,
mainkeeps Postgres busy on every core and its latency swings from run to run. This branch keeps about 1,900 of the 2,000 agents in a model call, so its throughput is close to what the agent count allows. With realistic idle time, this branch uses about a fifth ofmain's Postgres CPU per call, and every latency improves.With realistic idle time, this branch scales linearly to about 10,000 agents (271 model calls/s, tool result to next request 371 ms p50). Past that, the laptop runs out of CPU and memory for the workers, the fake model and the kernel, while Postgres stays under two cores. Raising claim loops to 16, or connections per worker to 32, doesn't raise that ceiling.
Behavior notes
Pending input cap
An agent holds at most 50 pending content inputs, counting queued and steering together.
agent_backlog_full(HTTP 409). The check runs under the agent lock after the idempotency lookup, so an identical retry still succeeds, and it reads at most 50 index entries.Deployment
Migration 052 requires a writer-stop maintenance window:
The migration builds every existing agent's execution state from its history, validates every agent, and then drops the old frontier functions. If it finds malformed or ambiguous history, it fails and rolls back with the offending agent IDs. Runtime leases and in-flight external work are preserved; normal recovery handles interrupted work after startup. The details are in
docs/self-hosting/deployment.mdx.Testing
make golangci-lint,make verify-static, andmake sqlc-check sql-rules sqlc-vet sqlc-vet-db migration-check migration-compat-check.make test-integration-storage,test-integration-runtimeandtest-integration-httpapi.-race.Excluding the one-time migration, handwritten production code is about 200 lines smaller than on
main, and generated code is about 2,850 lines smaller. Most of this PR's added lines are tests.🤖 Generated with Claude Code