Add concurrency guards to harness.Run: single-flight, semaphore and timeouts
CI / ci (push) Successful in 1m34s
CI / ci (pull_request) Successful in 1m37s

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>
This commit is contained in:
2026-10-05 10:00:20 -04:00
co-authored by Claude Opus 5.5
parent 87316d8b70
commit d03068ace5
10 changed files with 591 additions and 17 deletions
+10
View File
@@ -162,6 +162,16 @@ automatic** (D-021).
with key-like query parameters are removed, inside JSON strings too.
`harness.Purge` deletes rows older than `harness.RunRetention` (30 days),
clearing the `suggestion_batches` and `week_plans` references first.
- **Concurrency guards** (`harness.Guard`, one per process): `harness.Run`
takes a scope key, and concurrent runs of one skill with one scope share a
single in-flight run and its result, error included (single-flight); runs
with different scopes are independent. A semaphore lets at most
`harness.DefaultMaxCalls` (2, set by `NewGuard`) LLM calls run at once;
waiting for a slot honours the context. Each skill declares a timeout
(`harness.DefaultTimeout`, 60 s, unless it needs another) bounding the
whole run, slot waits and repair turn included; a run past it fails with
`llm.ErrTimeout`. A caller whose context ends gets the context error; the
shared run is cancelled, freeing its slot, once no caller waits for it.
- **Prompt layering** (D-007): each skill version's prompt is a
`text/template` file embedded from `ai/skills/prompts/`
(`<skill>.v<version>.tmpl`). The system prompt is always, in order: