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>