Skip to content

Commit a73cced

Browse files
ksarangmathclaude
andcommitted
perf: scale agent execution with typed execution state, a claim dispatcher, 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>
1 parent 5392608 commit a73cced

391 files changed

Lines changed: 28790 additions & 22339 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.golangci.yml‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,11 @@ linters:
141141
exclusions:
142142
generated: disable
143143
rules:
144-
- path: internal/storage/internal/storeutil/pool\.go
145-
source: '\.Query\(ctx, sql, args\.\.\.\)'
144+
- path: internal/storage/internal/agentexecution/internal/executiondb/batch\.go
145+
linters:
146+
- intrange
147+
- path: internal/storage/internal/(storeutil/pool|agentexecution/database)\.go
148+
source: '(\.Query\(ctx, (sql|query), args\.\.\.\)|b\.BatchResults\.Query\(\))'
146149
linters:
147150
- rowserrcheck
148151
# oapi-codegen's oneOf wrappers intentionally mix value accessors with
@@ -180,7 +183,7 @@ linters:
180183
- linters:
181184
- lll
182185
source: '^\s*\)\.[A-Za-z]'
183-
- path: (internal/storage/internal/dbsqlc/|internal/machinedaemon/statedb/internal/dbsqlc/)
186+
- path: (internal/storage/internal/dbsqlc/|internal/storage/internal/agentexecution/internal/executiondb/|internal/machinedaemon/statedb/internal/dbsqlc/)
184187
linters:
185188
- errcheck
186189
- govet

‎Makefile‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ MIGRATION_CHECK ?= $(GO) -C tools/ci run ./migrationcheck
2323
OMNARAD_VERSION ?= 0.0.0-dev
2424
MIGRATION_DIRS := migrations internal/machinedaemon/statedb/migrations
2525
SQLC_OWNED_PATHS := internal/storage/internal/dbsqlc internal/storage/queries \
26+
internal/storage/internal/agentexecution/internal/executiondb internal/storage/internal/agentexecution/queries \
2627
internal/machinedaemon/statedb/internal/dbsqlc internal/machinedaemon/statedb/queries
2728
OMNARALINT_SOURCES := $(shell find tools/omnaralint -name '*.go' -print)
2829
GO_MODULE_DIRS := . observability tools/ci tools/goose tools/omnaralint
@@ -259,6 +260,7 @@ sqlc-check:
259260

260261
sql-rules:
261262
CGO_ENABLED=0 $(GO) -C tools/ci run ./sqlrules ../../internal/storage/queries
263+
CGO_ENABLED=0 $(GO) -C tools/ci run ./sqlrules ../../internal/storage/internal/agentexecution/queries
262264

263265
sqlc-vet:
264266
$(SQLC) vet -f sqlc.yaml
@@ -267,7 +269,7 @@ migrate-test-db:
267269
$(TEST_DB_ENV) $(GO) test -count=1 -run '^TestMigrateLocalDatabase$$' ./internal/testutil/integrationdb
268270

269271
sqlc-vet-db:
270-
SQLC_DATABASE_URL=$${SQLC_DATABASE_URL:-$${OMNARA_TEST_DATABASE_URL:-$(TEST_DATABASE_URL)}} $(SQLC) vet -f sqlc.vet-db.yaml
272+
OMNARA_TEST_DATABASE_URL=$${SQLC_DATABASE_URL:-$${OMNARA_TEST_DATABASE_URL:-$(TEST_DATABASE_URL)}} $(GO) test -count=1 -tags=integration ./internal/storage/internal/agentexecution -run '^TestExecutionSQLCPrepare$$'
271273

272274
sqlc-vet-local-db: db-up migrate-test-db sqlc-vet-db
273275

‎cmd/worker/main.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,13 @@ func main() {
241241
StreamLog: log,
242242
})
243243
kernelWorker := workerpkg.NewWorker(store.Execution(), executor, workerpkg.Options{
244+
Continuation: &workerpkg.ContinuationOptions{
245+
MaxModelStarts: cfg.WorkerContinuationMaxModelStarts,
246+
MaxDuration: cfg.WorkerContinuationMaxDuration,
247+
},
244248
Log: log,
245249
Capacity: cfg.WorkerCapacity,
250+
ClaimConcurrency: cfg.WorkerClaimConcurrency,
246251
AsyncToolCapacity: cfg.WorkerAsyncToolCapacity,
247252
ControlSubscriber: redisBus,
248253
})

‎compose.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ services:
126126
OMNARA_WORKER_EVENT_WEBHOOK_CONCURRENCY: ${OMNARA_WORKER_EVENT_WEBHOOK_CONCURRENCY:-128}
127127
OMNARA_EVENT_WEBHOOK_PER_ORG_CONCURRENCY: ${OMNARA_EVENT_WEBHOOK_PER_ORG_CONCURRENCY:-128}
128128
OMNARA_WORKER_CAPACITY: ${OMNARA_WORKER_CAPACITY:-4}
129+
OMNARA_WORKER_CLAIM_CONCURRENCY: ${OMNARA_WORKER_CLAIM_CONCURRENCY:-4}
129130
OMNARA_WORKER_INBOX_CAPACITY: ${OMNARA_WORKER_INBOX_CAPACITY:-4}
130131
OMNARA_WORKER_DISCORD_CAPACITY: ${OMNARA_WORKER_DISCORD_CAPACITY:-1024}
131132
OMNARA_WORKER_ASYNC_TOOL_CAPACITY: ${OMNARA_WORKER_ASYNC_TOOL_CAPACITY:-32}

‎docs/self-hosting/configuration.mdx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,22 @@ All configuration is via environment variables. Use `.env.example` in the repo a
2828
| `OMNARA_WORKER_EVENT_WEBHOOK_CONCURRENCY` | Maximum event webhook deliveries in flight per worker process | `128` | No |
2929
| `OMNARA_EVENT_WEBHOOK_PER_ORG_CONCURRENCY` | Soft cap on concurrent event webhook deliveries per org across workers; use the same value on every worker | `128` | No |
3030
| `OMNARA_WORKER_CAPACITY` | Maximum agent turns executed concurrently by each worker process | `4` | No |
31+
| `OMNARA_WORKER_CLAIM_CONCURRENCY` | Maximum claim transactions each worker process runs at once; keep it small, since claims share one queue | `4` | No |
32+
| `OMNARA_WORKER_CONTINUATION_MAX_MODEL_STARTS` | Additional model calls a worker may start for an agent it already owns before releasing it back to the queue; `0` releases after every step | `2` | No |
33+
| `OMNARA_WORKER_CONTINUATION_MAX_DURATION` | Longest a worker keeps continuing an owned agent, checked between steps; never interrupts a running step (Go duration) | `30s` | No |
3134
| `OMNARA_WORKER_INBOX_CAPACITY` | Maximum integration inbox receipts processed concurrently per worker process; integer from 1 through 100 | `4` | No |
3235
| `OMNARA_WORKER_ASYNC_TOOL_CAPACITY` | Maximum runtime-owned asynchronous tool calls in flight per worker process | `32` | No |
3336
| `OMNARA_WORKER_BACKGROUND_TOOL_CAPACITY` | Background tool concurrency and in-memory queue capacity per worker process | `8` | No |
3437
| `OMNARA_MAINTENANCE_INTERVAL` | Maintenance loop interval (Go duration) | `1s` | No |
3538

39+
### Agent execution
40+
41+
Agents have typed execution state maintained by the execution store. Each transition
42+
updates domain records and publishes the agent's execution state and wakeup in the
43+
same transaction. Workers claim wakeups under fenced runtime leases and may
44+
continue ready work within the configured continuation budget. Rollbacks discard
45+
all pending state and notifications.
46+
3647
### Integration inbox operation
3748

3849
Inbox concurrency is independent of agent-turn concurrency. All worker activity

‎docs/self-hosting/deployment.mdx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ Open `http://localhost:8000` and sign up — the local email driver is `console`
2525

2626
## Production database upgrades
2727

28+
The execution-state migration requires a writer-stop maintenance window. Stop API,
29+
worker, maintenance, and daemon-report handlers before running migrations, and
30+
restart them together on the new release after migration succeeds. Existing agent
31+
history is backfilled before the migration completes. Do not allow an older binary
32+
to resume writing, delete runtime leases, or clear external tool ownership during
33+
the upgrade. Unfinished work recovers through the normal lease and retry paths.
34+
2835
Run the release's one-shot `migrate` image before starting its API, worker, and
2936
maintenance services. Connect the migrator directly to the PostgreSQL writer;
3037
do not use RDS Proxy or transaction-pooling PgBouncer. Long-running services

‎internal/agentmessage/message.go‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package agentmessage
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
func Render(displayName, key, childPublicID, kind, text string) string {
9+
label := fmt.Sprintf(
10+
"Subagent %q (agent_id %s, key %q)", displayName, childPublicID, key,
11+
)
12+
var header string
13+
switch kind {
14+
case "result":
15+
header = label + " finished its turn:"
16+
case "refused":
17+
header = label + " stopped because its model refused to continue; it produced no final answer:"
18+
case "content_filtered":
19+
header = label + " stopped because its model output was blocked by a content filter; " +
20+
"it produced no final answer:"
21+
case "failed":
22+
header = label + " failed:"
23+
case "question":
24+
header = label + " asked a question and is paused until a human answers it. " +
25+
"Messaging it with send_agent_message cancels the question."
26+
default:
27+
header = label + ":"
28+
}
29+
if strings.TrimSpace(text) == "" {
30+
return header
31+
}
32+
return header + "\n\n" + text
33+
}

‎internal/compaction/runner.go‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ func (r Runner) run(
131131
}
132132
resolved, err := r.Resolver.Resolve(ctx, selection)
133133
if err != nil {
134-
if ctx.Err() != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
134+
if ctx.Err() != nil &&
135+
(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
135136
return RunResult{}, err
136137
}
137138
return r.recordPreSendFailure(
@@ -265,7 +266,8 @@ func (r Runner) run(
265266
providerAttempt.ProviderRequestStarted = true
266267
providerAttempt.Response = response
267268
if err != nil {
268-
if ctx.Err() != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
269+
if ctx.Err() != nil &&
270+
(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
269271
return RunResult{}, err
270272
}
271273
if _, classified := model.ClassifyError(err); !classified {
@@ -401,7 +403,7 @@ func (r Runner) run(
401403
},
402404
)
403405
if err != nil {
404-
if errors.Is(err, executionstore.ErrCheckpointBoundaryUnsafe) {
406+
if errors.Is(err, storeerr.ErrCheckpointBoundaryUnsafe) {
405407
nextEnd, adjustmentErr := r.nextSmallerSourceEnd(ctx, input.Plan)
406408
if adjustmentErr != nil {
407409
return RunResult{}, errors.Join(err, adjustmentErr)
@@ -509,7 +511,10 @@ func (r Runner) validate(input RunInput) error {
509511
return nil
510512
}
511513

512-
func (r Runner) resultForUnclaimed(ctx context.Context, claim executionstore.ModelCallClaim) (RunResult, error) {
514+
func (r Runner) resultForUnclaimed(
515+
ctx context.Context,
516+
claim executionstore.ModelCallClaim,
517+
) (RunResult, error) {
513518
result := RunResult{
514519
ModelCallContextID: claim.Context.ID,
515520
}

‎internal/compaction/runner_candidate_test.go‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/omnara-ai/omnara/internal/events"
1010
"github.com/omnara-ai/omnara/internal/model"
1111
"github.com/omnara-ai/omnara/internal/storage/executionstore"
12+
"github.com/omnara-ai/omnara/internal/storage/storeerr"
1213
"github.com/stretchr/testify/require"
1314
)
1415

@@ -114,7 +115,7 @@ func TestRunnerReplansWhenPublishFindsUnsafeBoundary(t *testing.T) {
114115
textCompactionEvent(1, strings.Repeat("first closed event ", 20)),
115116
textCompactionEvent(2, strings.Repeat("second closed event ", 20)),
116117
},
117-
publishErrs: []error{executionstore.ErrCheckpointBoundaryUnsafe},
118+
publishErrs: []error{storeerr.ErrCheckpointBoundaryUnsafe},
118119
}
119120
client := &summaryModel{results: []summaryResult{
120121
{response: completeSummaryResponse("Concise first summary.")},
@@ -307,8 +308,10 @@ func TestSafeCompactionSourceEndsRequireModelVisibleSemantics(t *testing.T) {
307308
}
308309

309310
func TestRenderEventSourceReducesProjectionWithoutMutatingCanonicalOutput(t *testing.T) {
310-
original := json.RawMessage(`[{"type":"structured_data","value":{"stdout":"` + strings.Repeat("x", 40_960) +
311-
`TAIL"}}]`)
311+
original := json.RawMessage(
312+
`[{"type":"structured_data","value":{"stdout":"` + strings.Repeat("x", 40_960) +
313+
`TAIL"}}]`,
314+
)
312315
event := mustCompactionEvent(1, "tool_result", "completed", append(json.RawMessage(nil), original...))
313316
event.ToolName = "run_command"
314317
event.ProviderCallID = "call_1"
@@ -446,8 +449,10 @@ func TestCompactionAllowanceFitPrefersWholeTurnAndPreservesPartialProgress(t *te
446449
}{
447450
{name: "whole turn fits reduced allowance", firstText: "short request", secondText: largeText,
448451
firstKind: string(events.KindAgentInput), wantEnd: 2, wantReduced: true},
449-
{name: "oversized turn keeps preferred partial", firstText: "short request", secondText: largeText + largeText,
450-
firstKind: string(events.KindAgentInput), wantEnd: 1},
452+
{name: "oversized turn keeps preferred partial",
453+
firstText: "short request",
454+
secondText: largeText + largeText,
455+
firstKind: string(events.KindAgentInput), wantEnd: 1},
451456
{name: "oversized turn keeps reduced partial", firstText: largeText, secondText: largeText,
452457
firstKind: string(events.KindModelOutput), wantEnd: 1, wantReduced: true},
453458
{name: "preferred whole turn wins", firstText: "small output", secondText: largeText,
@@ -497,7 +502,10 @@ func (m summaryModelWithExplicitCapabilities) Capabilities() model.Capabilities
497502
func TestCompactionRetainsOverflowLocator(t *testing.T) {
498503
path := "/artifacts/art_overflow"
499504
parts, err := json.Marshal([]map[string]any{
500-
{"type": "structured_data", "value": map[string]any{"path": path, "preview": strings.Repeat("x", 8192)}},
505+
{
506+
"type": "structured_data",
507+
"value": map[string]any{"path": path, "preview": strings.Repeat("x", 8192)},
508+
},
501509
{"type": "text", "text": strings.Repeat("other content", 5000)},
502510
})
503511
if err != nil {

‎internal/config/config.go‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ type Config struct {
6666
AllowInsecureDev bool
6767
WorkerEventWebhookConcurrency int
6868
EventWebhookPerOrgConcurrency int
69+
WorkerContinuationMaxModelStarts int
70+
WorkerContinuationMaxDuration time.Duration
6971
WorkerCapacity int
72+
WorkerClaimConcurrency int
7073
WorkerInboxCapacity int
7174
WorkerDiscordCapacity int
7275
WorkerAsyncToolCapacity int
@@ -214,10 +217,22 @@ func Load() (Config, error) {
214217
if err != nil {
215218
return Config{}, err
216219
}
220+
workerContinuationMaxModelStarts, err := getenvInt("OMNARA_WORKER_CONTINUATION_MAX_MODEL_STARTS", 2)
221+
if err != nil {
222+
return Config{}, err
223+
}
224+
workerContinuationMaxDuration, err := time.ParseDuration(getenv("OMNARA_WORKER_CONTINUATION_MAX_DURATION", "30s"))
225+
if err != nil {
226+
return Config{}, fmt.Errorf("parse OMNARA_WORKER_CONTINUATION_MAX_DURATION: %w", err)
227+
}
217228
workerCapacity, err := getenvInt("OMNARA_WORKER_CAPACITY", 4)
218229
if err != nil {
219230
return Config{}, err
220231
}
232+
workerClaimConcurrency, err := getenvInt("OMNARA_WORKER_CLAIM_CONCURRENCY", 4)
233+
if err != nil {
234+
return Config{}, err
235+
}
221236
workerInboxCapacity, err := getenvInt("OMNARA_WORKER_INBOX_CAPACITY", 4)
222237
if err != nil {
223238
return Config{}, err
@@ -266,7 +281,10 @@ func Load() (Config, error) {
266281
AllowInsecureDev: os.Getenv("OMNARA_ALLOW_INSECURE_DEV_DEFAULTS") == "1",
267282
WorkerEventWebhookConcurrency: workerEventWebhookConcurrency,
268283
EventWebhookPerOrgConcurrency: eventWebhookPerOrgConcurrency,
284+
WorkerContinuationMaxModelStarts: workerContinuationMaxModelStarts,
285+
WorkerContinuationMaxDuration: workerContinuationMaxDuration,
269286
WorkerCapacity: workerCapacity,
287+
WorkerClaimConcurrency: workerClaimConcurrency,
270288
WorkerInboxCapacity: workerInboxCapacity,
271289
WorkerDiscordCapacity: workerDiscordCapacity,
272290
WorkerAsyncToolCapacity: workerAsyncToolCapacity,
@@ -622,9 +640,18 @@ func (cfg Config) ValidateWorker() error {
622640
if cfg.EventWebhookPerOrgConcurrency <= 0 {
623641
return fmt.Errorf("OMNARA_EVENT_WEBHOOK_PER_ORG_CONCURRENCY must be positive")
624642
}
643+
if cfg.WorkerContinuationMaxModelStarts < 0 {
644+
return fmt.Errorf("OMNARA_WORKER_CONTINUATION_MAX_MODEL_STARTS must be non-negative")
645+
}
646+
if cfg.WorkerContinuationMaxDuration <= 0 {
647+
return fmt.Errorf("OMNARA_WORKER_CONTINUATION_MAX_DURATION must be positive")
648+
}
625649
if cfg.WorkerCapacity <= 0 {
626650
return fmt.Errorf("OMNARA_WORKER_CAPACITY must be positive")
627651
}
652+
if cfg.WorkerClaimConcurrency <= 0 {
653+
return fmt.Errorf("OMNARA_WORKER_CLAIM_CONCURRENCY must be positive")
654+
}
628655
if cfg.WorkerInboxCapacity < 1 || cfg.WorkerInboxCapacity > 100 {
629656
return fmt.Errorf("OMNARA_WORKER_INBOX_CAPACITY must be between 1 and 100")
630657
}

0 commit comments

Comments
 (0)