feat(ADR-0161.2): HITL queue backpressure — pending-count cap + queue_full reports (#311)

This commit is contained in:
Shay 2026-05-26 16:16:08 -07:00 committed by GitHub
parent ac77b88864
commit 76032db9a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 489 additions and 16 deletions

View file

@ -1364,6 +1364,21 @@ def cmd_teaching_propose(args: argparse.Namespace) -> int:
)
except ProposalError as exc:
_die(f"ineligible: {exc}", code=1)
from teaching.proposals import RefusedAtCapacity
if isinstance(proposal, RefusedAtCapacity):
try:
rel_path = proposal.report_path.relative_to(_REPO_ROOT)
except ValueError:
try:
rel_path = proposal.report_path.relative_to(Path.cwd())
except ValueError:
rel_path = proposal.report_path
print(f"queue_full: pending={proposal.pending_count}, cap={proposal.cap}")
print("candidates_skipped: 1")
print(f"report_written: {rel_path}")
return 1
rec = log.find(proposal.proposal_id)
print(f"proposal_id : {proposal.proposal_id}")
print(f"state : {rec['state']}")
@ -1453,6 +1468,21 @@ def cmd_teaching_propose_from_exemplars(args: argparse.Namespace) -> int:
f"ineligible candidate for {corpus.shape_category.value}: {exc}",
code=1,
)
from teaching.proposals import RefusedAtCapacity
if isinstance(proposal, RefusedAtCapacity):
try:
rel_path = proposal.report_path.relative_to(_REPO_ROOT)
except ValueError:
try:
rel_path = proposal.report_path.relative_to(Path.cwd())
except ValueError:
rel_path = proposal.report_path
print(f"queue_full: pending={proposal.pending_count}, cap={proposal.cap}")
print("candidates_skipped: 1")
print(f"report_written: {rel_path}")
return 1
rec = log.find(proposal.proposal_id)
result = {
"shape_category": corpus.shape_category.value,

View file

@ -364,9 +364,10 @@ existing recorded queue history:
### Step 2 — Backpressure (pending-count cap)
- `propose_from_candidate` consults pending count via
`teaching/queue.derive_queue` and emits a `queue_full` report
instead of a new proposal when the cap is reached.
- `propose_from_candidate` in `teaching/proposals.py` consults pending count via
`teaching.queue.derive_queue` and writes a `queue_full` report at
`contemplation/runs/<timestamp>_queue_full.json` instead of a new proposal
when the cap is reached, returning `RefusedAtCapacity`.
- `contemplation/runs/<timestamp>.json` schema extended with
`report_kind ∈ {"learning_arc", "queue_full"}` (default
`"learning_arc"` for back-compat).

55
docs/hitl-backpressure.md Normal file
View file

@ -0,0 +1,55 @@
# HITL review queue backpressure
To prevent reviewed proposal queues from growing beyond human attention limits, the queue enforces a pending count cap (ADR-0161 §4).
## Configuration
- **Default Cap**: 256 pending proposals.
- **Environment Override**: To temporarily raise the cap, set the `CORE_HITL_PENDING_CAP` environment variable:
```bash
export CORE_HITL_PENDING_CAP=512
```
Accepted, rejected, and withdrawn proposals do not count toward the cap.
## Queue Full Reports
When the pending count is at or above the cap, `propose_from_candidate` will refuse to create a new proposal and instead emit a `queue_full` report to the contemplation runs directory:
`contemplation/runs/<ISO-8601-UTC>_queue_full.json`
Example `queue_full` report shape:
```json
{
"report_kind": "queue_full",
"emitted_at_revision": "9a738a16...",
"pending_count": 256,
"cap": 256,
"candidates_skipped": [
{
"candidate_id": "8a9b2c3d...",
"shape_category": "factual",
"reason": "queue_full"
}
]
}
```
The CLI exits with code `1` when capacity is refused, alerting CI runners and operators of the backpressure event.
## Operator Clearance Loop
When the queue is full and proposals are skipped:
1. **Review Pending Proposals**: Inspect the current queue using:
```bash
core teaching hitl-queue list --state pending
```
2. **Clear Space**: Ratify or withdraw pending proposals:
```bash
# Accept a proposal
core teaching review <proposal_id> --accept --review-date 2026-05-26
# Or withdraw a proposal
core teaching review <proposal_id> --withdraw
```
3. **Re-run Propose**: Once pending count falls below the cap, re-run the propose command. Skipped candidates will land successfully as fresh proposals with the same deterministic `proposal_id`.

View file

@ -45,6 +45,10 @@ if TYPE_CHECKING:
DEFAULT_PROPOSAL_LOG_PATH: Path = (
Path(__file__).resolve().parent / "proposals" / "proposals.jsonl"
)
DEFAULT_PENDING_CAP: int = 256
DEFAULT_CONTEMPLATION_RUNS_DIR: Path = (
Path(__file__).resolve().parent.parent / "contemplation" / "runs"
)
ReviewState = Literal["pending", "accepted", "rejected", "withdrawn"]
@ -116,6 +120,15 @@ class TeachingChainProposal:
}
@dataclass(frozen=True, slots=True)
class RefusedAtCapacity:
candidate_id: str
shape_category: str
pending_count: int
cap: int
report_path: Path
class ProposalError(ValueError):
"""Raised when a candidate fails an eligibility gate or when a
review action is attempted in a state that does not allow it."""
@ -453,7 +466,8 @@ def propose_from_candidate(
run_replay: Any = None,
allow_evaluative: bool = False,
source: ProposalSource | None = None,
) -> TeachingChainProposal:
cap: int | None = None,
) -> TeachingChainProposal | RefusedAtCapacity:
"""End-to-end: build proposal, run replay-equivalence gate,
auto-reject on regression, otherwise leave pending.
@ -473,6 +487,69 @@ def propose_from_candidate(
existing = log.find(proposal.proposal_id)
if existing is not None:
return proposal
if log.path == DEFAULT_PROPOSAL_LOG_PATH:
contemplation_runs_dir = DEFAULT_CONTEMPLATION_RUNS_DIR
else:
if (log.path.parent / "runs").exists():
contemplation_runs_dir = log.path.parent / "runs"
elif (log.path.parent / "contemplation" / "runs").exists():
contemplation_runs_dir = log.path.parent / "contemplation" / "runs"
else:
contemplation_runs_dir = log.path.parent / "runs"
from teaching.queue import derive_queue
queue_items = derive_queue(log, contemplation_runs_dir)
pending_count = sum(1 for item in queue_items if item.state == "pending")
resolved_cap = cap
if resolved_cap is None:
import os
env_val = os.environ.get("CORE_HITL_PENDING_CAP")
if env_val is not None:
try:
resolved_cap = int(env_val)
except ValueError:
resolved_cap = DEFAULT_PENDING_CAP
else:
resolved_cap = DEFAULT_PENDING_CAP
if pending_count >= resolved_cap:
shape_category = (
candidate.proposed_chain.get("recognizer_spec", {}).get("shape_category")
if candidate.proposed_chain
else None
) or candidate.claim_domain
from datetime import datetime, timezone
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%SZ")
report_path = contemplation_runs_dir / f"{stamp}_queue_full.json"
report_data = {
"report_kind": "queue_full",
"emitted_at_revision": _current_revision(),
"pending_count": pending_count,
"cap": resolved_cap,
"candidates_skipped": [
{
"candidate_id": candidate.candidate_id,
"shape_category": shape_category,
"reason": "queue_full"
}
]
}
contemplation_runs_dir.mkdir(parents=True, exist_ok=True)
report_path.write_text(
json.dumps(report_data, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8"
)
return RefusedAtCapacity(
candidate_id=candidate.candidate_id,
shape_category=shape_category,
pending_count=pending_count,
cap=resolved_cap,
report_path=report_path,
)
log.record_created(proposal)
if run_replay is None:
@ -557,9 +634,11 @@ def withdraw_proposal(
__all__ = [
"DEFAULT_PENDING_CAP",
"DEFAULT_PROPOSAL_LOG_PATH",
"ProposalError",
"ProposalLog",
"RefusedAtCapacity",
"ReplayEvidence",
"ReviewState",
"TeachingChainProposal",

View file

@ -45,19 +45,29 @@ def _load_contemplation_mapping(runs_dir: Path, runs_dir_mtime: float) -> dict[s
continue
pids: set[str] = set()
top_pid = data.get("proposal_id")
if isinstance(top_pid, str):
pids.add(top_pid)
report_kind = data.get("report_kind", "learning_arc")
if report_kind == "learning_arc":
top_pid = data.get("proposal_id")
if isinstance(top_pid, str):
pids.add(top_pid)
scenes = data.get("scenes")
if isinstance(scenes, list):
for scene in scenes:
if isinstance(scene, dict):
detail = scene.get("detail")
if isinstance(detail, dict):
scene_pid = detail.get("proposal_id")
if isinstance(scene_pid, str):
pids.add(scene_pid)
scenes = data.get("scenes")
if isinstance(scenes, list):
for scene in scenes:
if isinstance(scene, dict):
detail = scene.get("detail")
if isinstance(detail, dict):
scene_pid = detail.get("proposal_id")
if isinstance(scene_pid, str):
pids.add(scene_pid)
elif report_kind == "queue_full":
candidates_skipped = data.get("candidates_skipped", [])
if isinstance(candidates_skipped, list):
for cand in candidates_skipped:
if isinstance(cand, dict):
cand_id = cand.get("candidate_id")
if isinstance(cand_id, str):
pids.add(cand_id)
for pid in pids:
mapping[pid] = str(path.resolve())

View file

@ -0,0 +1,298 @@
"""Tests for ADR-0161 Step 2: HITL review queue backpressure (cap + reports)."""
from __future__ import annotations
import json
import os
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import patch
import pytest
from teaching.discovery import DiscoveryCandidate, EvidencePointer
from teaching.proposals import (
DEFAULT_PENDING_CAP,
ProposalLog,
RefusedAtCapacity,
ReplayEvidence,
build_proposal,
propose_from_candidate,
)
from teaching.queue import derive_queue
def make_candidate(candidate_id: str, subject: str, obj: str = "truth") -> DiscoveryCandidate:
return DiscoveryCandidate(
candidate_id=candidate_id,
proposed_chain={
"subject": subject,
"intent": "cause",
"connective": "reveals",
"object": obj,
},
trigger="would_have_grounded",
source_turn_trace="trace_1",
pack_consistent=True,
boundary_clean=True,
polarity="affirms",
claim_domain="factual",
evidence=(
EvidencePointer(
source="corpus",
ref="some_chain",
polarity="affirms",
epistemic_status="coherent",
),
),
)
def _fake_replay_equivalent(chain):
return ReplayEvidence(
baseline={"intent_accuracy": 1.0},
candidate={"intent_accuracy": 1.0},
regressed_metrics=(),
replay_equivalent=True,
)
def test_default_pending_cap_is_pinned():
"""ADR-0161 §4: default pending cap is strictly 256.
Any changes to this default require an ADR amendment.
"""
assert DEFAULT_PENDING_CAP == 256
def test_capacity_boundaries(tmp_path: Path):
log_path = tmp_path / "proposals.jsonl"
log = ProposalLog(log_path)
runs_dir = tmp_path / "runs"
runs_dir.mkdir()
# Pre-populate log with 2 pending proposals
for i in range(2):
c = make_candidate(f"cand_{i}", f"subject_{i}")
proposal = build_proposal(c)
log.record_created(proposal)
# cap = 3. Current pending is 2 (which is cap - 1)
# The 3rd candidate should land successfully
c3 = make_candidate("cand_2", "subject_2")
res3 = propose_from_candidate(
c3, log=log, run_replay=_fake_replay_equivalent, cap=3
)
assert not isinstance(res3, RefusedAtCapacity)
assert log.find(res3.proposal_id)["state"] == "pending"
# Current pending is now 3 (which is at cap).
# The 4th candidate should be refused
c4 = make_candidate("cand_3", "subject_3")
res4 = propose_from_candidate(
c4, log=log, run_replay=_fake_replay_equivalent, cap=3
)
assert isinstance(res4, RefusedAtCapacity)
assert res4.candidate_id == "cand_3"
assert res4.pending_count == 3
assert res4.cap == 3
assert res4.report_path.exists()
# Verify queue_full report contents
report = json.loads(res4.report_path.read_text(encoding="utf-8"))
assert report["report_kind"] == "queue_full"
assert report["pending_count"] == 3
assert report["cap"] == 3
assert len(report["candidates_skipped"]) == 1
assert report["candidates_skipped"][0]["candidate_id"] == "cand_3"
assert report["candidates_skipped"][0]["shape_category"] == "factual"
assert report["candidates_skipped"][0]["reason"] == "queue_full"
# Verify that the 4th proposal was NOT written to the log
assert log.find(build_proposal(c4).proposal_id) is None
def test_over_cap_refuses_consistently(tmp_path: Path):
log_path = tmp_path / "proposals.jsonl"
log = ProposalLog(log_path)
# Pre-populate log with 5 pending proposals
for i in range(5):
c = make_candidate(f"cand_{i}", f"subject_{i}")
proposal = build_proposal(c)
log.record_created(proposal)
# With cap = 3 (current pending = 5), we are already over capacity
# It should refuse consistently
c_new = make_candidate("cand_new", "subject_new")
res = propose_from_candidate(
c_new, log=log, run_replay=_fake_replay_equivalent, cap=3
)
assert isinstance(res, RefusedAtCapacity)
assert res.pending_count == 5
assert res.cap == 3
def test_core_hitl_pending_cap_env_var_override(tmp_path: Path):
log_path = tmp_path / "proposals.jsonl"
log = ProposalLog(log_path)
# Pre-populate log with 2 pending proposals
for i in range(2):
c = make_candidate(f"cand_{i}", f"subject_{i}")
proposal = build_proposal(c)
log.record_created(proposal)
# Set env var CORE_HITL_PENDING_CAP = 2. Current pending = 2
# The 3rd candidate should be refused
c3 = make_candidate("cand_2", "subject_2")
with patch.dict(os.environ, {"CORE_HITL_PENDING_CAP": "2"}):
res = propose_from_candidate(
c3, log=log, run_replay=_fake_replay_equivalent
)
assert isinstance(res, RefusedAtCapacity)
assert res.cap == 2
assert res.pending_count == 2
def test_explicit_cap_kwarg_overrides_env_var(tmp_path: Path):
log_path = tmp_path / "proposals.jsonl"
log = ProposalLog(log_path)
for i in range(2):
c = make_candidate(f"cand_{i}", f"subject_{i}")
proposal = build_proposal(c)
log.record_created(proposal)
c3 = make_candidate("cand_2", "subject_2")
# env var is 2, but kwarg is 5. Should successfully propose
with patch.dict(os.environ, {"CORE_HITL_PENDING_CAP": "2"}):
res = propose_from_candidate(
c3, log=log, run_replay=_fake_replay_equivalent, cap=5
)
assert not isinstance(res, RefusedAtCapacity)
assert log.find(res.proposal_id)["state"] == "pending"
def test_repropose_same_candidate_post_clearance(tmp_path: Path):
log_path = tmp_path / "proposals.jsonl"
log = ProposalLog(log_path)
# 1. Propose candidate when space is available (cap = 2, current pending = 0)
c1 = make_candidate("cand_1", "subject_1")
res1 = propose_from_candidate(
c1, log=log, run_replay=_fake_replay_equivalent, cap=2
)
assert not isinstance(res1, RefusedAtCapacity)
proposal_id_original = res1.proposal_id
# 2. Add another pending to hit cap
c2 = make_candidate("cand_2", "subject_2")
propose_from_candidate(
c2, log=log, run_replay=_fake_replay_equivalent, cap=2
)
# Now pending count is 2, cap is 2.
# 3. Re-proposing candidate 1 should return the existing proposal record (idempotency)
# even though we are technically at capacity, because it is already in the log.
res1_again = propose_from_candidate(
c1, log=log, run_replay=_fake_replay_equivalent, cap=2
)
assert not isinstance(res1_again, RefusedAtCapacity)
assert res1_again.proposal_id == proposal_id_original
# 4. Transition candidate 1 to accepted (clearance)
log.record_transition(proposal_id_original, "accepted", "ratified")
# Now pending count is 1 (candidate 2 is still pending, candidate 1 is accepted).
# Since pending (1) < cap (2), we can propose a new candidate or re-propose candidate 1
# as a fresh proposal. Wait, re-proposing same candidate post-clearance:
# Under ADR-0151, proposal_id is content-deterministic.
# It lands as a fresh proposal with the SAME proposal_id.
res1_post_clear = propose_from_candidate(
c1, log=log, run_replay=_fake_replay_equivalent, cap=2
)
assert not isinstance(res1_post_clear, RefusedAtCapacity)
assert res1_post_clear.proposal_id == proposal_id_original
def test_queue_full_report_byte_stability(tmp_path: Path):
log_path = tmp_path / "proposals.jsonl"
log = ProposalLog(log_path)
runs_dir = tmp_path / "runs"
runs_dir.mkdir()
# Pre-populate log to hit cap
c = make_candidate("cand_0", "subject_0")
proposal = build_proposal(c)
log.record_created(proposal)
# Force a known revision and timestamp for byte-stability
with patch("teaching.proposals._current_revision", return_value="f00ba2"):
c1 = make_candidate("cand_1", "subject_1")
res1 = propose_from_candidate(
c1, log=log, run_replay=_fake_replay_equivalent, cap=1
)
assert isinstance(res1, RefusedAtCapacity)
report_text1 = res1.report_path.read_text(encoding="utf-8")
# Delete the report and re-run with same parameters to verify byte-stable output
res1.report_path.unlink()
with patch("teaching.proposals._current_revision", return_value="f00ba2"):
res2 = propose_from_candidate(
c1, log=log, run_replay=_fake_replay_equivalent, cap=1
)
assert isinstance(res2, RefusedAtCapacity)
report_text2 = res2.report_path.read_text(encoding="utf-8")
# The JSON string must be exactly identical
assert report_text1 == report_text2
# Check key ordering
parsed = json.loads(report_text1)
keys = list(parsed.keys())
assert keys == sorted(keys)
def snapshot_dir(directory: Path) -> dict[Path, bytes]:
snapshot = {}
if not directory.exists():
return snapshot
for path in directory.glob("**/*"):
if path.is_file():
snapshot[path] = path.read_bytes()
return snapshot
def test_capacity_refusal_does_not_mutate_system():
"""Verify capacity refusal does not mutate packs, corpus, or recognizer registry."""
project_root = Path(__file__).resolve().parent.parent
dirs = [
project_root / "teaching" / "cognition_chains",
project_root / "packs",
project_root / "language_packs" / "data",
]
before_snapshots = {}
for d in dirs:
before_snapshots[d] = snapshot_dir(d)
# Trigger a capacity refusal
tmp_log = ProposalLog(project_root / "teaching" / "proposals" / "temp_proposals.jsonl")
c = make_candidate("cand_x", "subject_x")
res = propose_from_candidate(
c, log=tmp_log, run_replay=_fake_replay_equivalent, cap=0
)
assert isinstance(res, RefusedAtCapacity)
# Clean up temp files if created in the directory
if tmp_log.path.exists():
tmp_log.path.unlink()
# Clean up generated runs file
if res.report_path.exists():
res.report_path.unlink()
# Assert no mutations occurred in system folders
for d in dirs:
after_snapshot = snapshot_dir(d)
assert after_snapshot == before_snapshots[d], f"Directory {d} was mutated!"