Skip to content

fix(raw-weights): advance past sealed epochs so pushes can be acknowledged - #69

Merged
echobt merged 3 commits into
mainfrom
fix/raw-weight-epoch-advance
Jul 30, 2026
Merged

fix(raw-weights): advance past sealed epochs so pushes can be acknowledged#69
echobt merged 3 commits into
mainfrom
fix/raw-weight-epoch-advance

Conversation

@echobt

@echobt echobt commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • After PR fix(raw-weights): rebuild expired pending and surface rejection reasons #68 cleared expired pending payloads, challenge clients rebuilt for the same wall-clock epoch master had already sealed, producing a permanent 409 conflicting raw weight payload / sealed loop.
  • Because no push ever received a matching ACK, last_epoch and acknowledged_at stayed null for both challenges.
  • This change makes the real push path take the next unsealed aggregation epoch from master and advance on 409 instead of retrying the same identity forever.

Root cause

Clients chose epochs with local wall-clock buckets (timestamp // interval). Master sealing force-advances past sealed identities (_next_seal_epoch). When pending expired, rebuild reused the sealed bucket with revision 1 and a new digest. Master rejected with 409; the client cleared pending and repeated. The ACK write path never ran.

The working hotpatch semantic is COALESCE(MAX(epoch),0)+1 from sealed aggregation_epochs. That is now exposed as a read-only master endpoint and consumed by both challenge clients.

Changes

  • Master: GET /internal/v1/aggregation/target-epoch (challenge bearer + slug) returns target_epoch, highest_sealed_epoch, max_future_epoch_ahead.
  • Prism and agent-challenge push_once: prefer master target epoch; on 409 sealed/stale/conflict, clear pending and bounded-retry with advanced epoch or revision.
  • Tests cover master target epoch, ACK cursor fields (last_epoch / last_snapshot_id / acknowledged_at), sealed 409 advance, conflict revision bump, and master-over-local-clock preference.

Test plan

  • RED then GREEN for new target-epoch and client advance/ACK tests
  • pytest tests/unit/test_raw_weight_ingress.py packages/challenges/prism/tests/test_raw_weight_push.py packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py — 48 passed
  • Full agent-challenge suite: 24 failed / 2313 passed (baseline ~24 / ~2307; zero new failures)
  • ruff check/format on touched paths

CI

  • Waiting for required checks to go green on this head SHA.

Prod deploy confirmation

  1. Deploy master image that includes the target-epoch route and both embedded challenge clients.
  2. Do not stop keepalive-weights.sh or burn-weights-5gzi-48h until real clients show ACKs.
  3. Confirm first non-null ledger ACK, e.g. in each challenge DB:
    • SELECT last_epoch, last_snapshot_id, acknowledged_at, pending_epoch FROM raw_weight_push_ledger WHERE id = 1;
    • Expect acknowledged_at and last_epoch non-null after one successful push interval.
  4. Master logs / metrics: accepted raw-weight pushes; 409 sealed loops should stop.
  5. Optional: curl -sS -H "Authorization: Bearer <challenge-token>" -H "X-Base-Challenge-Slug: prism" http://localhost:<master>/internal/v1/aggregation/target-epoch should return target_epoch = highest_sealed + 1.

Notes

  • Does not modify keepalive hotpatch scripts or on-chain submit paths.
  • Wall-clock epoch_fn remains a fallback only when the master target-epoch GET is unreachable.

Summary by CodeRabbit

  • New Features

    • Added a master endpoint for retrieving the next available aggregation epoch.
    • Raw-weight submissions now prioritize the master’s epoch and automatically recover from sealed-epoch and revision conflicts.
    • Added bounded retries to prevent repeated submission loops.
    • Explicit weight maps now preserve zero values.
  • Bug Fixes

    • Improved durable acknowledgement tracking after successful submissions.
    • Added stricter authentication for internal aggregation requests.
    • Preserved pending payloads safely during eligible retries.

echobt added 3 commits July 30, 2026 04:34
Challenge clients were inventing epochs from wall-clock buckets while the
sealer force-advances past sealed identities. A sealed-epoch retry loop
meant no raw-weight push was ever acknowledged. Add a read-only internal
endpoint that returns COALESCE(MAX(sealed), 0) + 1 so clients target an
epoch master will still accept.
Prefer GET /internal/v1/aggregation/target-epoch over local wall-clock.
On sealed/stale/conflict 409, clear pending and retry with a bounded
epoch or revision advance so the same sealed identity cannot loop.
Verify the ACK path writes last_epoch, last_snapshot_id, and
acknowledged_at.
… on 409

Mirror Prism: master-authoritative target epoch, bounded 409 identity
advance, and tests that a successful push records last_epoch plus
acknowledged_at. Stops the production sealed-epoch 409 loop that left
the ledger never acknowledged.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Raw weight push coordination

Layer / File(s) Summary
Master target-epoch contract and authorization
src/base/master/raw_weight_ingress.py, tests/unit/test_raw_weight_ingress.py
Adds TargetEpochInfo, computes the next unsealed epoch, validates challenge bearer credentials, and exposes the authenticated target-epoch endpoint with unit coverage.
Agent Challenge retry and durable acknowledgement flow
packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py, packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py
Adds master-preferred epoch resolution, bounded 409 epoch/revision advancement, pending payload reuse, explicit zero preservation, and acknowledgement cursor tests.
Prism retry and durable acknowledgement flow
packages/challenges/prism/src/prism_challenge/raw_weight_push.py, packages/challenges/prism/tests/test_raw_weight_push.py
Adds equivalent bounded retry, pending-state, epoch-selection, and durable acknowledgement behavior with Prism-specific coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RawWeightPushClient
  participant TargetEpochEndpoint
  participant RawWeightIngressService
  participant DurableCursor
  RawWeightPushClient->>TargetEpochEndpoint: Fetch authoritative target_epoch
  TargetEpochEndpoint->>RawWeightIngressService: Authorize challenge and compute epoch
  RawWeightIngressService-->>TargetEpochEndpoint: Return TargetEpochInfo
  TargetEpochEndpoint-->>RawWeightPushClient: Return target_epoch
  RawWeightPushClient->>RawWeightPushClient: POST raw-weight snapshot
  RawWeightPushClient->>DurableCursor: Persist acknowledged epoch and snapshot_id
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main fix: advancing past sealed epochs so raw-weight pushes can be acknowledged.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/raw-weight-epoch-advance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/base/master/raw_weight_ingress.py (1)

1100-1129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Declare a typed response_model for the new endpoint.

-> dict[str, Any] gives the generated OpenAPI an empty/unconstrained response schema, so the target-epoch contract clients now depend on is undocumented. A small Pydantic model (or reusing TargetEpochInfo fields) keeps the generated spec authoritative.

As per coding guidelines, "Treat OpenAPI generated by the code as the API source of truth".

♻️ Sketch
+class TargetEpochResponse(BaseModel):
+    target_epoch: int
+    highest_sealed_epoch: int | None
+    max_future_epoch_ahead: int
+
     `@router.get`(
         "/internal/v1/aggregation/target-epoch",
+        response_model=TargetEpochResponse,
         responses={
         },
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/base/master/raw_weight_ingress.py` around lines 1100 - 1129, Define a
typed Pydantic response model for the target-epoch payload and register it as
the response_model on get_aggregation_target_epoch. Replace the unconstrained
dict[str, Any] return annotation with that model, preserving the existing
target_epoch, highest_sealed_epoch, and max_future_epoch_ahead fields so OpenAPI
documents the endpoint contract.

Source: Coding guidelines

packages/challenges/prism/src/prism_challenge/raw_weight_push.py (1)

456-475: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Both clients fall back to the wall-clock epoch with no diagnostic when the lookup fails.

_fetch_master_target_epoch returns None for any non-200 or exception without logging, so a misconfigured token or unreachable master silently restores the stale-bucket behavior this PR fixes.

  • packages/challenges/prism/src/prism_challenge/raw_weight_push.py#L456-L475: log status code / exception type (never the token) before returning None.
  • packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py#L446-L465: apply the same logging (diff provided in the per-site comment there).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/challenges/prism/src/prism_challenge/raw_weight_push.py` around
lines 456 - 475, Update _fetch_master_target_epoch in
packages/challenges/prism/src/prism_challenge/raw_weight_push.py (lines 456-475)
and its sibling in
packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py
(lines 446-465) to log diagnostic context before returning None: include the
non-200 response status code or exception type, and never include the shared
token or other secret headers. Preserve the existing fallback behavior for
invalid responses and failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py`:
- Around line 439-501: Update the sealed-epoch tests in
packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py:439-501 and
packages/challenges/prism/tests/test_raw_weight_push.py:561-615 so the mocked
GET returns epoch 49 on the initial query and 50 on the re-query after the 409;
then assert the exact POST sequence is [49, 50], replacing vacuous count-based
assertions while preserving the acknowledged result and final cursor checks.

---

Nitpick comments:
In `@packages/challenges/prism/src/prism_challenge/raw_weight_push.py`:
- Around line 456-475: Update _fetch_master_target_epoch in
packages/challenges/prism/src/prism_challenge/raw_weight_push.py (lines 456-475)
and its sibling in
packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py
(lines 446-465) to log diagnostic context before returning None: include the
non-200 response status code or exception type, and never include the shared
token or other secret headers. Preserve the existing fallback behavior for
invalid responses and failures.

In `@src/base/master/raw_weight_ingress.py`:
- Around line 1100-1129: Define a typed Pydantic response model for the
target-epoch payload and register it as the response_model on
get_aggregation_target_epoch. Replace the unconstrained dict[str, Any] return
annotation with that model, preserving the existing target_epoch,
highest_sealed_epoch, and max_future_epoch_ahead fields so OpenAPI documents the
endpoint contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f4e7d54-53fa-4145-914a-494f809bcfe7

📥 Commits

Reviewing files that changed from the base of the PR and between a9704cc and ef9af42.

📒 Files selected for processing (6)
  • packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py
  • packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py
  • packages/challenges/prism/src/prism_challenge/raw_weight_push.py
  • packages/challenges/prism/tests/test_raw_weight_push.py
  • src/base/master/raw_weight_ingress.py
  • tests/unit/test_raw_weight_ingress.py

Comment on lines +439 to +501
@pytest.mark.asyncio
async def test_sealed_epoch_409_advances_to_master_target(database: Database) -> None:
"""Sealed-epoch 409 must not infinite-retry the same epoch; advance via master."""

clock = FakeClock()
post_epochs: list[int] = []

def handler(request: httpx.Request) -> httpx.Response:
if request.method == "GET":
# Master says next unsealed is 50 after first sealed rejection path.
return httpx.Response(
200,
json={
"target_epoch": 50,
"highest_sealed_epoch": 49,
"max_future_epoch_ahead": 2,
},
)
parsed = RawWeightPushRequest.model_validate_json(request.content)
post_epochs.append(parsed.epoch)
if parsed.epoch == 49:
return httpx.Response(
409, json={"detail": "epoch is sealed; revision rejected"}
)
return httpx.Response(
200,
json={
"protocol_version": "1.0",
"challenge_slug": SLUG,
"epoch": parsed.epoch,
"revision": parsed.revision,
"snapshot_id": f"snap-{parsed.epoch}",
"payload_digest": parsed.payload_digest,
"accepted": True,
"idempotent": False,
},
)

http = httpx.AsyncClient(
transport=httpx.MockTransport(handler),
base_url="http://master.test",
)
client = RawWeightPushClient(
database=database,
challenge_slug=SLUG,
master_base_url="http://master.test",
shared_token=TOKEN,
now_fn=clock.now,
http_client=http,
# Stale local clock would stick on sealed 49 forever without master.
epoch_fn=lambda: 49,
)
await client.init()
with activate_role(Role.CHALLENGE):
result = await client.push_once(weights={WINNER: 1.0})
assert result.status == "acknowledged"
assert result.epoch == 50
assert 49 in post_epochs or post_epochs == [50]
# Must not hammer the same sealed epoch repeatedly in one push_once.
assert post_epochs.count(49) <= 1
assert await client.store.get_cursor() is not None
assert (await client.store.get_cursor()).epoch == 50
await http.aclose()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The sealed-epoch tests never trigger a 409, so the advance branch is untested.

In both tests the mocked GET returns target_epoch: 50, and _resolve_target_epoch prefers it over epoch_fn, so the very first POST is already epoch 50 and the 409 handler is never entered. post_epochs.count(49) <= 1 (and 49 in post_epochs or post_epochs == [50]) then pass vacuously. Make the first GET return the stale/sealed epoch (or 409 the first POST at 50 once) so the identity-advance path is genuinely covered.

  • packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py#L439-L501: script the GET to return 49 first and 50 on the re-query, then assert post_epochs == [49, 50].
  • packages/challenges/prism/tests/test_raw_weight_push.py#L561-L615: same scripting, and assert the exact posted epoch sequence rather than a count(...) <= 1 bound.
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 478-478: Configuring an LLM/agent client endpoint over http:// sends prompts and responses (and often API keys) in cleartext, exposing them to interception. Use https for the base_url.
Context: base_url="http://master.test"
Note: [CWE-319] Cleartext Transmission of Sensitive Information.

(llm-client-insecure-http-python)


[warning] 478-478: Do not make http calls without encryption
Context: "http://master.test"
Note: [CWE-319] Cleartext Transmission of Sensitive Information.

(requests-http)


[warning] 483-483: Do not make http calls without encryption
Context: "http://master.test"
Note: [CWE-319] Cleartext Transmission of Sensitive Information.

(requests-http)

📍 Affects 2 files
  • packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py#L439-L501 (this comment)
  • packages/challenges/prism/tests/test_raw_weight_push.py#L561-L615
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py` around
lines 439 - 501, Update the sealed-epoch tests in
packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py:439-501 and
packages/challenges/prism/tests/test_raw_weight_push.py:561-615 so the mocked
GET returns epoch 49 on the initial query and 50 on the re-query after the 409;
then assert the exact POST sequence is [49, 50], replacing vacuous count-based
assertions while preserving the acknowledged result and final cursor checks.

@echobt
echobt merged commit 8249563 into main Jul 30, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant