All checks were successful
deploy / deploy (push) Successful in 6m1s
Closes #3 and #4. Both specs had gaps that only appeared on contact, which is worth recording because it is evidence for the plan-quality question in #10: - `enqueue(&[DiscoveredIssue])` cannot know a JobKind. It is derivable from labels, so the store now carries the LabelProtocol and core gained `job_kind_for`. The precedence when an operator applies several mode labels had to be decided: plan beats implement, because planning produces the implementation children and so loses nothing, while the reverse silently discards the decomposition that was also asked for. - `claim_next(worker, allowed_lanes)` had no lane to filter on. Migration 0002 adds one, recorded at enqueue by `routing::lane_for` — the same function `route` now delegates to, so the two cannot drift. Deriving it at claim time instead would mean a forge request per claim, since labels are not stored. - `list_opted_in_issues` returned a bare Vec with nowhere to put the ETag that #4's own step 3 requires, so a caller could not make the next poll conditional. It returns an `IssuePage` now, which also carries `not_modified` — a 304 is not an empty repo, and a poller that conflated them would treat every quiet poll as every issue having disappeared. The claim is one statement: a CTE takes the row lock with SKIP LOCKED and the update writes the claim, so select and update share a transaction without managing one by hand. Returning a job to pending clears the claim, because `pending_holds_no_claim` refuses the half-done version — the database is what makes lease expiry safe rather than the code remembering to. Enum values round-trip through serde rather than a hand-written match, so the schema's check constraints and the Rust types are provably one vocabulary. Also replaced a test from #2 that asserted exactly one migration exists. It failed on the first legitimate migration, which teaches people to edit the assertion rather than think. It now asserts what it was reaching for: versions unique and ascending. Verified against Postgres 18 — 15 database tests covering enqueue idempotency across repeated polls, lane filtering including the agent:oc override, claim metadata, lease expiry and reclaim, terminal jobs refusing transition, renewal requiring you still hold the claim, and re-routing a queued job when its labels change. Plus 12 mock-forge tests: If-None-Match sent and 304 distinguished from empty, ETag surfaced, 429 retried but bounded, 503 retried then succeeding, 404 not retried, pull requests filtered out, and every unimplemented write failing without touching the forge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6
38 lines
1.9 KiB
SQL
38 lines
1.9 KiB
SQL
-- The lane a job will run on, recorded when it is enqueued.
|
|
--
|
|
-- `JobStore::claim_next` takes the lanes its caller is allowed to start, because
|
|
-- the governor caps them separately (design.md §5). Without this column that
|
|
-- filter cannot be expressed in SQL, and a runner would have to claim a job
|
|
-- before discovering it belongs to a lane that is currently held — taking the
|
|
-- row out of reach of the runner that could have run it.
|
|
--
|
|
-- Denormalised on purpose. The lane is derived from the job kind, whether it has
|
|
-- a parent, and any `tireless/agent:*` override on the issue — and the labels
|
|
-- are not stored, so deriving it at claim time would mean a forge request per
|
|
-- claim. `tireless_core::routing::lane_for` computes it once, at enqueue, and
|
|
-- the poller refreshes it when labels change. The value is therefore a cache of
|
|
-- an operator's expressed intent, which is exactly the accuracy the label
|
|
-- protocol already promises (§2.2).
|
|
--
|
|
-- Nullable, with no default: a job enqueued before this migration has no
|
|
-- recorded lane, and guessing one would be worse than leaving it unclaimable
|
|
-- until the next poll refreshes it. There are no such jobs today — nothing has
|
|
-- ever been enqueued — but the reasoning is what matters for the next migration
|
|
-- that adds a column the claim path depends on.
|
|
alter table job
|
|
add column lane text
|
|
check (lane in ('claude_code', 'opencode'));
|
|
|
|
-- Supersedes job_claimable_idx from 0001. The claim now filters on lane as well
|
|
-- as state, and still orders by created_at alone so `LIMIT 1` can stop at the
|
|
-- first match rather than sorting the pending set — the property measured in
|
|
-- #2 at 4 buffers versus 720.
|
|
--
|
|
-- Lane leads because it is an equality filter with two values, so it partitions
|
|
-- the index cleanly; created_at follows to give the ordering for free.
|
|
drop index job_claimable_idx;
|
|
|
|
create index job_claimable_idx
|
|
on job (lane, created_at)
|
|
where state = 'pending';
|