Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0142f61
fix(route): advance free candidates after explicit 429
seonghobae Sep 25, 2026
0cf0a3c
test(route): cover 429 storm with default retry budget
seonghobae Sep 25, 2026
abca479
fix(route): re-run selection when a skipped cooldown lapses mid-storm
seonghobae Sep 26, 2026
0f307a7
fix(tests): restore main's test signal; re-apply dropped OpenRouter a…
seonghobae Sep 26, 2026
343bf7f
fix(embeddings): preserve unbounded provider wait
seonghobae Sep 26, 2026
ea1ac22
docs: record unbounded embedding wait repair
seonghobae Sep 26, 2026
86d4e83
docs: bind protected-main test recovery evidence
seonghobae Sep 26, 2026
6e70a19
fix(ci): bind rustfmt and clippy to pinned toolchain
seonghobae Sep 26, 2026
824959a
docs(changelog): record pinned Rust components
seonghobae Sep 26, 2026
227d6f6
docs(gap): record pinned Rust gate repair
seonghobae Sep 26, 2026
e8ea56c
fix(route): restore request-local policy and candidate evidence
seonghobae Sep 26, 2026
740096d
fix(route): keep streaming and quality evidence on one revision
seonghobae Sep 26, 2026
96de901
fix(ci): restore pinned runtime and baseline test gates
seonghobae Sep 26, 2026
097036e
fix(provider): retain typed allowlist rejection
seonghobae Sep 26, 2026
f882ee7
fix(provider): preserve transport on allowlist failures
seonghobae Sep 26, 2026
3686c25
Merge gate recovery before 429 routing review
seonghobae Sep 26, 2026
0ce43cc
fix(batch): wake embedding waiters on backend close
seonghobae Sep 26, 2026
080677a
test(batch): expect cancellation after backend close
seonghobae Sep 26, 2026
572830c
test(routing): preserve unrelated error after cooldown expiry
seonghobae Sep 26, 2026
887e2f0
test(routing): reject synthetic 429 cooldown authority
seonghobae Sep 26, 2026
b6a6afb
test(routing): retain bounded retry for unheadered 429
seonghobae Sep 26, 2026
c7ab118
fix(rate-limit): fail closed without provider retry timing
seonghobae Sep 26, 2026
8b4f743
merge: reconcile headerless 429 admission evidence
seonghobae Sep 26, 2026
a8ecb22
fix(ci): restore runtime audit and locked wheel build
seonghobae Sep 26, 2026
0165407
test(route): reproduce Retry-After zero retry regression
seonghobae Sep 26, 2026
01efad9
fix(route): honor provider-authorized immediate retry
seonghobae Sep 26, 2026
2241ac2
test(route): keep zero-delay quota retry out of circuit
seonghobae Sep 26, 2026
cb05873
fix(route): retry zero-delay quota without circuit penalty
seonghobae Sep 26, 2026
db73a0c
test(route): make zero-delay quota fixture free-eligible
seonghobae Sep 26, 2026
a406675
test(batch): reproduce durable claim renewal after close
seonghobae Sep 26, 2026
2bd7b70
test(batch): fence result immediately after close
seonghobae Sep 26, 2026
e6707f7
fix(batch): stop durable claim renewal on shutdown
seonghobae Sep 26, 2026
a89f269
fix(batch): fence provider result after shutdown
seonghobae Sep 26, 2026
09dbae1
docs(route): scope Retry-After to finite provider timing
seonghobae Sep 26, 2026
82f9ba2
docs(gap): record shutdown claim fence evidence
seonghobae Sep 26, 2026
c93fc2e
test(batch): reproduce late failure publication after close
seonghobae Sep 26, 2026
709d4ca
fix(batch): fence late failure after shutdown
seonghobae Sep 26, 2026
66caf62
docs(gap): record shutdown failure fence evidence
seonghobae Sep 26, 2026
4041f5a
docs(gap): restore complete baseline after transfer
seonghobae Sep 26, 2026
cae8c58
fix(ci): audit locked VCS environment without pip hash mode
seonghobae Sep 27, 2026
aca0c85
fix(ci): audit locked security tooling dependencies
seonghobae Sep 27, 2026
bb1fae3
fix(security): audit runtime lock on 429 branch
seonghobae Sep 27, 2026
8e9999c
fix(batch): fence shutdown publication and observe durable cancellation
seonghobae Sep 27, 2026
9d66898
test(routing): verify mixed quota recovery without replaying other fa…
seonghobae Sep 27, 2026
f620bad
merge: reconcile quota recovery and lifecycle fences with current main
seonghobae Sep 27, 2026
c5150bd
merge: carry independently tracked mixed quota regression repair
seonghobae Sep 27, 2026
8dae079
merge: integrate document review main and preserve quota recovery reg…
seonghobae Sep 27, 2026
675ce18
Merge branch 'main' of https://github.com/ContextualWisdomLab/context…
seonghobae Sep 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .github/workflows/nim-benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,15 @@ jobs:
with:
python-version: "3.12"

- name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.5"

- name: Install pinned runtime
run: |
python -m pip install --require-hashes -r requirements.lock
python -m pip install --no-deps -e .
uv sync --locked --extra api --extra db --extra queue
echo "$PWD/.venv/bin" >> "$GITHUB_PATH"

- name: Run dry benchmark
env:
Expand Down Expand Up @@ -104,10 +109,15 @@ jobs:
with:
python-version: "3.12"

- name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.5"

- name: Install pinned runtime
run: |
python -m pip install --require-hashes -r requirements.lock
python -m pip install --no-deps -e .
uv sync --locked --extra api --extra db --extra queue
echo "$PWD/.venv/bin" >> "$GITHUB_PATH"

- name: Resolve live parameters
id: live_params
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/opencode-hourly-loop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,16 @@ jobs:
with:
node-version: "22"

- name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.5"

- name: Install pinned runtime dependencies
timeout-minutes: 10
run: python -m pip install --require-hashes -r requirements.lock
run: |
uv sync --locked --extra api --extra db --extra queue
echo "$PWD/.venv/bin" >> "$GITHUB_PATH"

- name: Install OpenCode CLI
timeout-minutes: 10
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/provider-catalog-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,15 @@ jobs:
with:
python-version: "3.12"

- name: Install hash-pinned runtime dependencies
run: python -m pip install --disable-pip-version-check --no-input --require-hashes -r requirements.lock
- name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.5"

- name: Install pinned runtime dependencies
run: |
uv sync --locked --extra api --extra db --extra queue
echo "$PWD/.venv/bin" >> "$GITHUB_PATH"

- name: Select durable or run-scoped PostgreSQL KV
shell: bash
Expand Down
41 changes: 31 additions & 10 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ jobs:

- name: Install hash-locked quality tools
run: |
python -m pip install --require-hashes -r requirements.lock
python -m pip install --require-hashes -r requirements-opencode-review-ci.txt
uv pip install --python .venv/bin/python --require-hashes -r requirements-opencode-review-ci.txt
echo "$PWD/.venv/bin" >> "$GITHUB_PATH"

- name: Prove complete benchmark coverage and public docstrings
run: |
Expand All @@ -95,9 +95,9 @@ jobs:
run: |
set -euo pipefail
rm -rf dist "$RUNNER_TEMP/nim-wheel-site"
python -m pip wheel --no-deps --no-build-isolation . --wheel-dir dist
uv build --wheel --no-build-isolation --out-dir dist
python scripts/verify_decision_wheel_manifest.py dist/contextual_orchestrator-*.whl "$RUNNER_TEMP"/decision-wheels/*.whl
python -m pip install --no-deps \
uv pip install --python .venv/bin/python --no-deps \
--target "$RUNNER_TEMP/nim-wheel-site" \
dist/contextual_orchestrator-*.whl "$RUNNER_TEMP"/decision-wheels/*.whl
cd "$RUNNER_TEMP"
Expand All @@ -123,8 +123,16 @@ jobs:
with:
python-version: "3.12"

- name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.5"

- name: Install fuzz dependencies
run: python -m pip install --require-hashes -r requirements.lock -r fuzz/requirements-property.txt -r fuzz/requirements-atheris.txt
run: |
uv sync --locked --extra api --extra db --extra queue --group dev
uv pip install --python .venv/bin/python --require-hashes -r fuzz/requirements-property.txt -r fuzz/requirements-atheris.txt
echo "$PWD/.venv/bin" >> "$GITHUB_PATH"

- name: Run property-based fuzz tests
run: python -m pytest tests/fuzz -q
Expand Down Expand Up @@ -266,17 +274,30 @@ jobs:
with:
python-version: "3.12"

- name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.5"

- name: Install audit and project dependencies
run: |
python -m pip install --require-hashes -r requirements-security-ci.txt
python -m pip install --require-hashes -r requirements.lock
python -m pip install --no-deps -e .
uv sync --locked --extra api --extra db --extra queue --group dev --no-install-project
uv sync --locked --extra api --extra db --extra queue --group dev --no-build-isolation
python -m venv "$RUNNER_TEMP/security-tools"
"$RUNNER_TEMP/security-tools/bin/python" -m pip install --require-hashes -r requirements-security-ci.txt

- name: Audit pinned dependencies and generate SBOM
run: |
set -euo pipefail
python -m pip_audit -r requirements.lock
cyclonedx-py environment --output-format json --output-file cyclonedx-sbom.json
"$RUNNER_TEMP/security-tools/bin/pip-audit" --require-hashes -r requirements-security-ci.txt
audit_lock="${RUNNER_TEMP}/requirements-audit.txt"
python scripts/ci/prepare_runtime_lock_audit.py requirements.lock uv.lock "$audit_lock"
"$RUNNER_TEMP/security-tools/bin/pip-audit" --require-hashes --disable-pip -r "$audit_lock"
"$RUNNER_TEMP/security-tools/bin/pip-audit" --path "$PWD/.venv/lib/python3.12/site-packages" --format json --output "$RUNNER_TEMP/dependency-audit.json"
jq -e '([.dependencies[] | select(.skip_reason) | .name] | sort) == ["contextual-orchestrator", "fast-mlsirm"]' "$RUNNER_TEMP/dependency-audit.json"
"$RUNNER_TEMP/security-tools/bin/cyclonedx-py" environment "$PWD/.venv/bin/python" --output-format json --output-file cyclonedx-sbom.json
jq -e 'any(.components[]; .name == "contextual-orchestrator")' cyclonedx-sbom.json
jq -e '[.components[] | select(.name == "fast-mlsirm") | (.purl // "") | contains("09f762ded35786dd1078222a4577ff09d649816f")] == [true]' cyclonedx-sbom.json

- name: Upload CycloneDX SBOM
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # actions/upload-artifact@v7.0.1
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.d/rate-limit-aware-admission.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ candidate, while an explicit concrete model id keeps failing fast
unconditionally, regardless of how many failover candidates exist --
preserving the `tests/test_provider_error_taxonomy.py` single-candidate,
no-header 429 contract that must never wait.

An explicit quota rejection advances to the next eligible candidate before a same-agent tool retry. Unknown-duration 429 recovery retains the existing administrator-owned assumed cooldown and labels it as assumed; it does not become a provider-declared timing claim.
3 changes: 3 additions & 0 deletions CHANGELOG.d/restore-test-signal-main.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Restore the OpenRouter availability collector's separation from the answer-quality ledger. The restack merge for #1074 (`3078949c`) had silently reverted `openrouter_uptime.py` to its pre-`b3be48e3` form, so uptime polls once again rewrote the quality router's prior and blended in the benchmark quality prior. The collector now updates only the transport ledger again. The fixed `_UPTIME_FETCH_TIMEOUT_SECONDS` bound from #971 is kept. Also repair test drift that stopped the hosted `Tests and package quality` job at collection or kept suites red: the renamed provider-embedding claim-lease constant, a fake HTTP connection without `connect()`/`sock`, fingerprinted discovered-agent ids, a fake binary response without `headers.get`, and a stale batch-retention assertion that contradicted the unbounded-execution contract. Provider embedding waits now accept the application default `None` as an unbounded deadline, matching the synchronous endpoint contract instead of raising `TypeError` before provider completion.
Bind `rustfmt` and `clippy` to the repository's pinned Rust 1.97.1 toolchain. The minimal rustup profile does not include either component, so the hosted Rust gate previously selected the directory override and failed before formatting or linting could run.
Audit the hashed runtime index pins alongside the installed environment, and require the non-PyPI Git source to agree across locks before the security gate can pass.
4 changes: 4 additions & 0 deletions contextual_orchestrator/batch_job_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ def lock(
*,
lease_seconds: float | None = None,
renew_until_epoch: float | None = None,
renew_while: Callable[[], bool] | None = None,
):
"""Return an atomic shard claim with bounded lease and acquisition wait."""
lock_name = f"batch_job_registry:{name}:claim:{key}"
Expand Down Expand Up @@ -245,6 +246,9 @@ def acquired_claim():
def renew_claim() -> None:
interval = _claim_renewal_interval_seconds(lease_seconds)
while not stop_renewal.wait(interval):
if renew_while is not None and not renew_while():
lease.mark_lost()
return
remaining = renew_until_epoch - time.time()
if remaining <= 0:
lease.mark_lost()
Expand Down
82 changes: 54 additions & 28 deletions contextual_orchestrator/batch_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1088,9 +1088,17 @@ def __init__(

def close(self) -> None:
"""Release the bounded worker pool owned by this backend."""
self._closed.set()
with self._executor_lock:
self._closed.set()
executor, self._executor = self._executor, None
for job_id, event in list(self._terminal_events.items()):
if self._registry.durable:
# The next backend can reclaim persisted work after this worker exits.
event.set()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
else:
self.cancel(
BatchJob(job_id=job_id, backend=self.name), reason="backend closed"
)
if executor is not None:
executor.shutdown(wait=False, cancel_futures=True)

Expand Down Expand Up @@ -1164,6 +1172,7 @@ def _run_job(self, job_id: str) -> None:
job_id,
lease_seconds=self._claim_lease_seconds,
renew_until_epoch=deadline_epoch,
renew_while=lambda: not self._closed.is_set(),
) as execution_claim:
self._run_claimed_job(job_id, execution_claim)
except ClaimNotAcquired:
Expand Down Expand Up @@ -1255,6 +1264,8 @@ def _run_claimed_job(self, job_id: str, execution_claim: Any) -> None:
requests = list(self._requests[job_id])
try:
vectors, prompt_tokens = self._runner(requests)
if self._closed.is_set():
execution_claim.mark_lost()
Comment thread
seonghobae marked this conversation as resolved.
execution_claim.ensure_owned()
if time.time() >= self._execution_deadline(job_id):
self._publish_terminal(
Expand Down Expand Up @@ -1295,6 +1306,9 @@ def _run_claimed_job(self, job_id: str, execution_claim: Any) -> None:
except ClaimNotAcquired:
raise
except Exception as exc: # noqa: BLE001 - polling exposes bounded failure metadata
if self._closed.is_set():
execution_claim.mark_lost()
execution_claim.ensure_owned()
error = {
"error_type": type(exc).__name__,
"http_status": getattr(
Expand All @@ -1320,32 +1334,36 @@ def _publish_terminal(
usage: Any = None,
error: Any = None,
) -> None:
"""Publish terminal state atomically for durable registries."""
if self._registry.durable:
self._registry.publish_provider_embedding_terminal(
execution_claim,
job_id,
status=status,
results=results,
usage=usage,
error=error,
)
return
with self._registry.lock(
"provider_embedding_job_states",
job_id,
lease_seconds=self._claim_lease_seconds,
):
"""Fence shutdown and publish one terminal outcome under the lifecycle lock."""
with self._executor_lock:
if self._closed.is_set():
execution_claim.mark_lost()
execution_claim.ensure_owned()
if self._states.get(job_id) not in {"queued", "running"}:
raise ClaimNotAcquired("provider embedding job is already terminal")
if results is not None:
self._results[job_id] = results
if usage is not None:
self._usage[job_id] = usage
if error is not None:
self._errors[job_id] = error
self._states[job_id] = status
if self._registry.durable:
self._registry.publish_provider_embedding_terminal(
execution_claim,
job_id,
status=status,
results=results,
usage=usage,
error=error,
)
return
with self._registry.lock(
"provider_embedding_job_states",
job_id,
lease_seconds=self._claim_lease_seconds,
):
execution_claim.ensure_owned()
if self._states.get(job_id) not in {"queued", "running"}:
raise ClaimNotAcquired("provider embedding job is already terminal")
if results is not None:
self._results[job_id] = results
if usage is not None:
self._usage[job_id] = usage
if error is not None:
self._errors[job_id] = error
self._states[job_id] = status

def wait(self, job: BatchJob, *, timeout: float | None) -> Dict[str, Any]:
"""Wait within the caller's explicit deadline for a terminal state.
Expand All @@ -1354,10 +1372,18 @@ def wait(self, job: BatchJob, *, timeout: float | None) -> Dict[str, Any]:
wall-clock deadline (contextual-orchestrator's no-implicit-deadline
default). ``threading.Event.wait`` raises ``OverflowError`` for a
non-finite timeout on CPython, so both forms are translated to ``None``
(block indefinitely) rather than passed through.
(block indefinitely) rather than passed through. Durable unbounded waits
observe shared terminal state at the backend's existing claim cadence;
another process cannot signal this process's local event.
"""
event = self._terminal_events.get(job.job_id)
if event is not None:
if event is not None and self._registry.durable and (
timeout is None or not math.isfinite(timeout)
):
while not self._closed.is_set() and not self.poll(job)["is_complete"]:
if event.wait(self.poll_after_ms / 1000):
break
elif event is not None:
event.wait(
timeout=(
timeout
Expand Down
Loading
Loading