Fail a run whose ai_runs row is saved after the skill timeout
CI / ci (push) Successful in 1m35s
CI / ci (pull_request) Successful in 1m36s

runOnce checked the deadline only before saving the row. The save can
wait for the store's single writer, so a run whose work ended in time
could return success after its timeout. Check the deadline again once
the row is saved and return llm.ErrTimeout when it has passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 10:21:10 -04:00
co-authored by Claude Opus 5.5
parent a6f750ea04
commit 7b81d48150
2 changed files with 31 additions and 0 deletions
+25
View File
@@ -295,6 +295,31 @@ func TestRunTimeout(t *testing.T) {
t.Fatalf("rows = %+v, want one provider_error row naming the timeout", rows)
}
})
t.Run("row saved late", func(t *testing.T) {
var f llm.Fake
cfg := testConfig(t, &f)
f.QueueGenerate(reply(`{"echo":"taco"}`))
timeout := 100 * time.Millisecond
// Holding the store's only writer connection makes the save wait
// past the deadline, after the work itself has ended in time.
conn, err := cfg.Store.Writer.Conn(context.Background())
if err != nil {
t.Fatal(err)
}
released := time.AfterFunc(3*timeout, func() { conn.Close() })
defer released.Stop()
out, err := Run(context.Background(), cfg, timeoutSkill{timeout: timeout}, "week-1", echoInput{Word: "taco"})
if !errors.Is(err, llm.ErrTimeout) {
t.Fatalf("err = %v, want ErrTimeout", err)
}
if out != (echoOutput{}) {
t.Fatalf("out = %+v, want no output", out)
}
assertCalls(t, &f, 1)
if rows := runRows(t, cfg.Store); len(rows) != 1 {
t.Fatalf("rows = %+v, want one row", rows)
}
})
t.Run("waiting for a slot", func(t *testing.T) {
var f llm.Fake
cfg := testConfig(t, &f)
+6
View File
@@ -186,6 +186,12 @@ func runOnce[In, Out any](ctx context.Context, cfg Config, s Skill[In, Out], tim
if err != nil {
return zero, err
}
// The save may wait past the deadline for the store's single writer.
// The run has then not ended in time, so it fails, though its row
// records the work's outcome as it stood before the save.
if !time.Now().Before(deadline) {
return zero, expired
}
return out, nil
}