Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/docs/content/docs/environments-and-files.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,10 @@ affect a later reservation or Turn. Evaluate deadlines after acquiring the Sessi
lock, and return terminal storage outcomes without rolling their transaction back.

A validated Runtime `failed` response with `preparation_failed` and no Run settles
the pending input immediately with `runtime_preparation_failed`. It records a
the pending input immediately with `runtime_preparation_failed`. Before admission,
a rejection of the current `execution_prepare` request with no Run and code
`invalid_configuration`, `unsupported_configuration` or `unsupported_preparation`
settles through that same failure path. It records a
safe Session failure before any Turn exists and releases the input gate; fixing
the local cause allows new input. Transport loss, capacity rejection and
unconfirmed cleanup remain retryable within the original deadline. Core uses
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/content/guide-sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"contracts/agents-api/execution-tools.md": "8cc0dbe207e8e80ac104bf482c37ea297cd64250b51d288ed4baf553756424d2",
"docs/api/public-agent-api.md": "00979732412a971013e8f01b4c74820ff51aafdded6b0f105d25a78af86a6627",
"docs/examples.md": "0e1bdaeff51c9c36779f817be31ea9816b7d8cb2290cf8350d3c4801f436f4f9",
"contracts/agents-api/environments.md": "424dac4bfd63418afc314896dd6a3e4c5c4375dd0a8bb7579920c5dd9743da22",
"contracts/agents-api/environments.md": "29be005ded0ad9fae226bd590b5becb880cc7de0380ce847f16a153e7c3c3195",
"docs/getting-started/nodes.md": "7c1b7e364ce85b5b5916358cafc618f29042b2125ac45653be665ba07e772fdb",
"docs/getting-started/self-hosted.md": "5ade597e09cbfa2e321f341693b47730b3696fe7bd4d6834eafbe6b279a55e6b",
"docs/self-hosted-native.md": "b3cf736f88792e6925c50b81a4c33eba6b9ee86e195f9ed4e2187db7bed71d88",
Expand Down Expand Up @@ -44,7 +44,7 @@
"content/docs/agents-and-tools.mdx": "44dfde4e3b3906b30323c2e75a89650ae4837210c7ba425be2266edf87ce8ff8",
"content/docs/sessions.mdx": "bb63d799ed90038836d652f3f866d0309822425114c6b2aa36654b8a991947f6",
"content/docs/examples.mdx": "587061e65ab2e841d14980539ba94e2216d4e8135c948a852b9a8bb0359c85d7",
"content/docs/environments-and-files.mdx": "6d8b2a5d5e95e3a5eff47ce93f0239c7cc38d4678bac53c58f5c8df016c23dc5",
"content/docs/environments-and-files.mdx": "88d1fc4129270302a1f4bebd7febfaac622f42dcb1f3f5524b4fa49e10106479",
"content/docs/hosted-providers.mdx": "9c241090709a9929ab6a34615db1e20a94c1f36649026281836060e81ac40b4c",
"content/docs/self-hosted-execution.mdx": "eeed4c6b3646927ccc3c7ac2d20b2c0f9c8a65e42d734310fe3959e016324e57",
"content/docs/self-hosted-native.mdx": "671451580dfb4f5c134221991f68292a4f992008174d23190b1da3742046571f",
Expand Down
5 changes: 4 additions & 1 deletion contracts/agents-api/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,10 @@ affect a later reservation or Turn. Evaluate deadlines after acquiring the Sessi
lock, and return terminal storage outcomes without rolling their transaction back.

A validated Runtime `failed` response with `preparation_failed` and no Run settles
the pending input immediately with `runtime_preparation_failed`. It records a
the pending input immediately with `runtime_preparation_failed`. Before admission,
a rejection of the current `execution_prepare` request with no Run and code
`invalid_configuration`, `unsupported_configuration` or `unsupported_preparation`
settles through that same failure path. It records a
safe Session failure before any Turn exists and releases the input gate; fixing
the local cause allows new input. Transport loss, capacity rejection and
unconfirmed cleanup remain retryable within the original deadline. Core uses
Expand Down
7 changes: 7 additions & 0 deletions services/agents-api/internal/execution/preparation.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ func (d *Dispatcher) awaitPreparation(ctx context.Context, tenant, session strin
}
status, err := prepared.observation(env)
if err != nil {
var rejection *preparationRejection
if status.RunID == "" && errors.As(err, &rejection) && rejection.operation == proto.TypeExecutionPrepare {
switch rejection.code {
case "invalid_configuration", "unsupported_configuration", "unsupported_preparation":
return pending, errPreparationFailed
}
}
return pending, err
}
if status.State == "ready" && status.RunID == "" && status.ExecutorID != "" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,62 +112,75 @@ func TestEnvironmentInputPromotionRollsBackHistoryAndSettlement(t *testing.T) {
}

func TestEnvironmentInputDeadlineIsCheckedAfterSessionLock(t *testing.T) {
s, pool := testStore(t)
lease := executionLease(t, s)
writer := lease.Store()
tenant, session := environmentInputSession(t, s)
pending := reserveEnvironmentInput(t, s, tenant, session.ID, "pending")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
tx, err := pool.Begin(ctx)
if err != nil {
t.Fatal(err)
}
defer func() { _ = tx.Rollback(context.Background()) }()
var blocker int32
if err := tx.QueryRow(ctx, "SELECT pg_backend_pid() FROM sessions WHERE id=$1 FOR UPDATE", session.ID).Scan(&blocker); err != nil {
t.Fatal(err)
}
type outcome struct {
value EnvironmentInputReservation
err error
}
done := make(chan outcome, 1)
go func() {
got, err := writer.PromoteEnvironmentInput(ctx, tenant, session.ID, pending.ID)
done <- outcome{got, err}
}()
for {
var blocked bool
if err := pool.QueryRow(ctx, "SELECT EXISTS (SELECT 1 FROM pg_stat_activity WHERE $1=ANY(pg_blocking_pids(pid)))", blocker).Scan(&blocked); err != nil {
t.Fatal(err)
}
if blocked {
break
}
select {
case result := <-done:
t.Fatal("promotion bypassed Session lock", result)
case <-ctx.Done():
t.Fatal("promotion lock wait not observed")
case <-time.After(5 * time.Millisecond):
}
}
// Transaction-start time is now older than the controlled deadline.
if _, err := tx.Exec(ctx, "UPDATE environment_input_reservations SET deadline=clock_timestamp() WHERE id=$1", pending.ID); err != nil {
t.Fatal(err)
}
if err := tx.Commit(ctx); err != nil {
t.Fatal(err)
}
result := <-done
if result.err != nil || result.value.State != EnvironmentInputExpired || result.value.SettledAt == nil {
t.Fatal("lock wait extended input lifetime", result)
}
environmentInputHistory(t, pool, session.ID, 0, 0)
stored, err := s.GetEnvironmentInputReservation(ctx, tenant, session.ID, pending.ID)
if err != nil || stored.State != EnvironmentInputExpired {
t.Fatal("expiry was rolled back", stored, err)
for _, action := range []string{"promote", "fail"} {
t.Run(action, func(t *testing.T) {
s, pool := testStore(t)
lease := executionLease(t, s)
writer := lease.Store()
tenant, session := environmentInputSession(t, s)
pending := reserveEnvironmentInput(t, s, tenant, session.ID, "pending")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
tx, err := pool.Begin(ctx)
if err != nil {
t.Fatal(err)
}
defer func() { _ = tx.Rollback(context.Background()) }()
var blocker int32
if err := tx.QueryRow(ctx, "SELECT pg_backend_pid() FROM sessions WHERE id=$1 FOR UPDATE", session.ID).Scan(&blocker); err != nil {
t.Fatal(err)
}
type outcome struct {
value EnvironmentInputReservation
err error
}
done := make(chan outcome, 1)
go func() {
var got EnvironmentInputReservation
var err error
if action == "promote" {
got, err = writer.PromoteEnvironmentInput(ctx, tenant, session.ID, pending.ID)
} else {
err = writer.FailEnvironmentInput(ctx, tenant, session.ID, pending.ID, "runtime_preparation_failed")
if err == nil {
got, err = s.GetEnvironmentInputReservation(ctx, tenant, session.ID, pending.ID)
}
}
done <- outcome{got, err}
}()
for {
var blocked bool
if err := pool.QueryRow(ctx, "SELECT EXISTS (SELECT 1 FROM pg_stat_activity WHERE $1=ANY(pg_blocking_pids(pid)))", blocker).Scan(&blocked); err != nil {
t.Fatal(err)
}
if blocked {
break
}
select {
case result := <-done:
t.Fatal("settlement bypassed Session lock", result)
case <-ctx.Done():
t.Fatal("settlement lock wait not observed")
case <-time.After(5 * time.Millisecond):
}
}
// Transaction-start time is now older than the controlled deadline.
if _, err := tx.Exec(ctx, "UPDATE environment_input_reservations SET deadline=clock_timestamp() WHERE id=$1", pending.ID); err != nil {
t.Fatal(err)
}
if err := tx.Commit(ctx); err != nil {
t.Fatal(err)
}
result := <-done
if result.err != nil || result.value.State != EnvironmentInputExpired || result.value.SettledAt == nil {
t.Fatal("lock wait extended input lifetime", result)
}
environmentInputHistory(t, pool, session.ID, 0, 0)
stored, err := s.GetEnvironmentInputReservation(ctx, tenant, session.ID, pending.ID)
if err != nil || stored.State != EnvironmentInputExpired {
t.Fatal("expiry was rolled back", stored, err)
}
})
}
}

Expand Down
3 changes: 3 additions & 0 deletions services/agents-api/internal/store/environment_inputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ func (s *Store) FailEnvironmentInput(ctx context.Context, tenantID, sessionID, r
return err
}
return s.withEnvironmentInputSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error {
if err := q.ExpireEnvironmentInputReservation(ctx, sqlc.ExpireEnvironmentInputReservationParams{SessionID: session, ID: id}); err != nil {
return err
}
_, err := q.FailEnvironmentInput(ctx, sqlc.FailEnvironmentInputParams{SessionID: session, ID: id, FailureCode: pgtype.Text{String: code, Valid: true}})
return err
})
Expand Down
Loading