The row-saved-late test only checked that one ai_runs row existed, so it
would also pass if the work itself had timed out before the save. It now
requires the row to be ok with no error, which holds only when the work
ended in time and the save alone ran past the deadline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
runOnce only mapped an expired context to llm.ErrTimeout when execute
returned an error, so a PostProcess that succeeded after the deadline was
reported as a success. Check the deadline after execution (context cause
or the clock, since the timer may not have fired yet) and fail such a run
with llm.ErrTimeout and a provider_error ai_runs row.
Adds a TestRunTimeout case with a skill whose PostProcess succeeds only
after its timeout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
harness.Run now takes a scope key. Concurrent runs of one skill with one
scope share a single in-flight run and its result, errors included, so
only one Generate call is made; different scopes run independently. A
process-wide Guard (NewGuard, DefaultMaxCalls = 2) bounds concurrent LLM
calls with a context-aware semaphore. Each skill declares Timeout()
(DefaultTimeout = 60 s), enforced via context over the whole run; a run
past it fails with an error matching llm.ErrTimeout. A caller whose
context ends gets the context error; the shared run is cancelled, and
its slot freed, once no caller waits for it.
llm.Fake results can now block (Release) and signal entry (Started), so
tests hold calls in flight.
Maestro task: 8f533721-32c9-4f73-af9f-9a9f0e94abf8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>