From 7b81d481505fddb686303bcf8847fde40dda669c Mon Sep 17 00:00:00 2001 From: dev_agent Date: Mon, 5 Oct 2026 10:21:10 -0400 Subject: [PATCH] Fail a run whose ai_runs row is saved after the skill timeout 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 --- internal/ai/harness/guard_test.go | 25 +++++++++++++++++++++++++ internal/ai/harness/harness.go | 6 ++++++ 2 files changed, 31 insertions(+) diff --git a/internal/ai/harness/guard_test.go b/internal/ai/harness/guard_test.go index ea86be9..f46d0a2 100644 --- a/internal/ai/harness/guard_test.go +++ b/internal/ai/harness/guard_test.go @@ -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) diff --git a/internal/ai/harness/harness.go b/internal/ai/harness/harness.go index df0ef1d..0ec9ee3 100644 --- a/internal/ai/harness/harness.go +++ b/internal/ai/harness/harness.go @@ -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 }