Skip to content

Shard projection drains by owner, push wakes, fresh first, head deadline - #24731

Open
Git-on-my-level wants to merge 2 commits into
mainfrom
claude/project-thread-936wun
Open

Git-on-my-level wants to merge 2 commits into
mainfrom
claude/project-thread-936wun

Conversation

@Git-on-my-level

@Git-on-my-level Git-on-my-level commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Requested by David · project thread

What changed and why

Today every backend-integration instance pages the same global committed_at keyset each 20 s and races the others for each owner's advisory claim. Most drain turns end claim_busy or find nothing, the GPUs sit near 20% busy, and backlog churn drives Firestore reads. This PR makes four changes, all behind default-off or default-unchanged switches.

  1. Owner-hash shard leases (utils/batch_pipeline/projection_shards.py). Each shard has a 30 s Redis lease held by one serving process, and the leases balance across members. Only the holder drains a shard's owners, so each owner has one drain writer.
  2. Push wakes and Firestore cuts (projection_shard_pump.py, serving.py).
    • Commits from Serving.drain_owner push a wake for the owner. Requests routed through Projector.request_drain become wakes for the shard holder.
    • The GPU worker's own merge commits push no wake. Instead, the maintenance leader follows the tail of recently committed rows.
    • Only the maintenance leader pages the result log. It does so discovery-only, from a resuming cursor, and runs the finish page. Followers skip the sweep.
  3. Fresh-first lanes. Each shard has a fresh lane (capture under 6 h old, newest first) and a backlog lane (oldest commit first). Two drain slots stay reserved for backlog while backlog waits. A stuck owner moves behind due owners, so it cannot fill the take page.
  4. Every non-terminal head has an exit (projection.py).
    • The 24 h bound on failing heads is now BATCH_PROJECTION_HEAD_DEADLINE_SECONDS, with the same default.
    • With BATCH_PROJECTION_BLOCKED_HEAD_DEADLINE_ENABLED, a never-passing ProjectionBlocked head on a closed allowlist takes the existing held disposition after the deadline: a quarantine_held receipt, audio and result kept, owned by the release scan and reviewed redrive. Nothing is deleted.

Ordering is unchanged. The transactional predecessor fence and the advisory owner claim still decide every assignment.

Each process fails open to the legacy sweep in any of these cases:

  • A lease pass fails.
  • A wake take fails.
  • One of the existing kill switches is off: owner chain, sweep owner chain, owner drain, or pending page.

Contract: backend/docs/listen_pusher_pipeline.mdx#batch-projection-owner-shards. Change note: backend/docs/listen_pusher_pipeline/changes/2026-11-05-projection-owner-shards.md.

Rollout. All new switches default to today's behavior. The intended order:

  1. BATCH_PROJECTION_SHARDS_ENABLED=true on staging backend-integration.
  2. The same on every prod backend-integration serving process in one config train.

The clean metrics are in the flag registry entry. Rollback is false, and the sweep returns on the next tick.

For BATCH_PROJECTION_BLOCKED_HEAD_DEADLINE_ENABLED, David answered "Yes unblock" in the project chat at 2026-11-05 18:46Z, and the coordinator relayed that as approval to enable dead-lettering of held rows in prod. Confirm with David before the prod flip. He asked for a nightly redrive and an alert on dead-letter volume alongside it. Those two are not in this PR.

This builds on the drain pump from #24712. It reuses that PR's BATCH_PIPELINE_MAX_OWNER_DRAINS, which stays at 8. Shards apply only to the serving loops. A dedicated projection pump process keeps its plain sweep. With BATCH_PIPELINE_SERVING_PROJECTION_SWEEP=off, the leader skips the tail and the backstop, but commit wakes still flow.

Out of scope:

Line-count exception

Line-Count-Exception: backend/utils/batch_pipeline/projection.py | 1702 -> 1909 | Blocked-head deadline branch and resolver, shard router hooks, and tail/backstop discovery on Projector; new shard logic already lives in projection_shards.py and projection_shard_pump.py. Follow-up split: move Projector discovery (sweep, discover_owners, discover_tail, sweep_finish_only) into utils/batch_pipeline/projection_discovery.py, owned by omi-pipeline.

Product invariants affected

none

How it was verified

Hermetic only. Nothing was run against staging or prod.

  • BACKEND_UNIT_TEST_FILE_LIST=<184 files> bash backend/test.sh exited 0 after rebasing on f8edd96cd. After rebasing again onto 5b8265c50 (which merged feat(batch): add opt-in dedicated projection drain pump #24712), the same set plus test_batch_pipeline_projection_pump.py and test_flag_budget.py passed. The 184 files are every selected backend unit test whose name matches batch_pipeline, projection, serving, firestore, flag, ratchet, gmp, registry, batch_stage, executor, async_block or redis.
  • The full selector requires the whole backend unit suite because database/batch_pipeline_store.py changed. CI runs that.
  • python3 scripts/check_feature_flag_registry.py reported OK.
  • scripts/scan_async_blockers.py --diff-base origin/main reported 0 findings.
  • scripts/backend-python-format (black 26.5.1) was applied.
  • An independent review pass found 8 issues, and all of them are fixed here:
    • GPU commits pushed no wake.
    • Kill switches were bypassed.
    • A failed take did not fail open.
    • A Redis call ran on the loop when deferral overflowed.
    • A settle could reset the sequence and lose a wake.
    • A failed drain task was treated as the chain end.
    • The stop token was wrong after a hold.
    • The deadline clock was shared with transient failures.

Needs a credentialed staging check before the prod flip:

  • Redis Lua EVAL on the serving Redis.
  • Lease balance across the real instance count (omi_batch_projection_shards_unheld is 0).
  • Wake depth and drain throughput against the pre-switch hour.

Tests

  • backend/tests/unit/test_batch_projection_shards.py (38 tests), against fakeredis with real Lua:
    • Leases: cover, balance, handover, join, observer, fail-open.
    • Wakes: lane ordering, the settle sequence race, deferral, the sequence never resetting.
    • Pump: fresh first, backlog reserve, stuck owners not filling the take page, claim-busy keeping its lane position, kill switches, local fallback for an unheld shard.
    • Discovery: tail discovery of GPU commits, the backstop.
    • Serving wiring.
    • Blocked-head deadline: off, on, ineligible messages, clock restart, end stop after a hold.
    • Drain-task error reporting.
  • backend/tests/unit/test_batch_projection_shard_drain.py: two serving processes with real OwnerDrains/Projector on one fakeredis.
    • Every owner drains once, in chain order, by its shard holder only.
    • Zero claim overlaps, and all wakes settle.
    • A foreign drain request is routed to its holder.

UI evidence

UI-Evidence: none -- backend only

Failure class (fixes)

Failure-Class: none

🤖 Generated with Claude Code

https://claude.ai/code/session_017CeWy13948MM5eAUJE7zqo

🤖 Generated with Claude Code

View guided diff Turn on auto-fix

Owner-hash shard leases in Redis give each owner one drain writer across
the fleet. Merge commits push wakes, and the maintenance leader follows the
committed tail and runs a discovery-only backstop. The per-instance sweep
is no longer needed while shards are engaged. Holders fill drain slots fresh
first and keep a reserve for backlog. A stuck owner yields its lane place.

Every Redis error, and every existing drain kill switch, returns that
process to the legacy sweep. The ordering fences are unchanged.

Eligible never-passing ProjectionBlocked heads get a deadline and take the
existing held disposition (audio kept, redrivable). This is behind
BATCH_PROJECTION_BLOCKED_HEAD_DEADLINE_ENABLED, default off.

All new switches default to current behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017CeWy13948MM5eAUJE7zqo
@Git-on-my-level Git-on-my-level self-assigned this Nov 5, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T22:24:33.515252Z 38305ed New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a30a19ff4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +230 to +233
drains = projector.ensure_drains()
free = drains.free_slots
if free <= 0:
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Admit waiting owners between productive drain turns

With shards enabled, once all drain slots are occupied by owners with long chains, this check prevents every other owner from being admitted. OwnerDrains.finished() immediately requeues productive continuations, so free_slots remains zero even at the 120-second turn boundary. Unlike the previous sweep, which queued other owners behind active turns, the shard pump cannot add waiting owners to that FIFO. Fresh captures and other backlog owners can therefore wait until an initially admitted owner's entire chain finishes, defeating both fresh-first scheduling and per-owner turn fairness. Allow bounded admission of waiting owners between continuations rather than requiring an empty execution slot.

Useful? React with 👍 / 👎.

Comment on lines +208 to +210
def blocked_head_type(error: BaseException) -> str:
"""Bounded, log-safe episode token for a deadline-eligible refusal."""
return f'ProjectionBlocked:{projection_block_reason(error)}'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Distinguish message-based blocked-head episodes

When the blocked-head deadline is enabled, all six message-based eligible refusals produce the same ProjectionBlocked:other token because their exceptions do not carry block_reason. If one refusal is resolved and a different eligible refusal is encountered—for example, a recovery handoff lands and assignment then discovers missing VAD accounting—resolve_blocked_head() reuses the old timestamp and can immediately quarantine the row instead of restarting the new refusal's deadline. Give each allowlisted message a distinct bounded episode token so the deadline measures time spent blocked for the same reason.

Useful? React with 👍 / 👎.

Comment on lines +251 to +255
except Exception as error:
self.healthy = False
self.held = set()
self.unheld = set(range(self.count))
logger.warning(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Record the shard-to-legacy fail-open transition

When Redis refresh fails, this branch marks sharding inactive and serving resumes the legacy per-instance sweep, but it only emits a warning. Neither this path nor the legacy-sweep caller invokes the required shared record_fallback helper, so operators cannot observe this mode change through omi_fallback_total even though it restores fleet-wide duplicate discovery and claim contention. Instrument the fail-open transition with the shared helper and an appropriate outcome, rather than relying solely on the shard warning.

AGENTS.md reference: backend/AGENTS.md:L322-L322

Useful? React with 👍 / 👎.

@cubic-dev-ai cubic-dev-ai 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.

19 issues found across 20 files

Confidence score: 2/5

  • In projection_shard_pump.py, a productive owner can keep requeueing itself after free_slots reaches zero, preventing other owners from getting a turn. Admit a bounded waiting set between continuations.
  • In batch_pipeline.py, the documented BATCH_PROJECTION_SHARDS_ENABLED=1 and on values do not enable shards in normal serving mode. Parse the documented truthy values before passing the legacy value through.
  • In projection.py, different eligible refusal types all become ProjectionBlocked:other, so changing refusal type does not restart the deadline. Encode each allowlisted refusal type distinctly.
  • In projection_shard_pump.py, pick can select a backlog wake before its retry time after a process restart, causing a stuck owner to retry immediately. Skip backlog wakes whose scores are still in the future.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/tests/unit/test_batch_projection_shards.py">

<violation number="1" location="backend/tests/unit/test_batch_projection_shards.py:23">
P2: These tests do not isolate all kill switches that control the behavior they assert. Clear `BATCH_PIPELINE_OWNER_DRAIN_ENABLED` and `BATCH_PIPELINE_SERVING_PROJECTION_SWEEP` in the autouse fixture (or set every required switch explicitly) so CI/deployment environment flags cannot change the test outcome.</violation>
</file>

<file name="backend/docs/listen_pusher_pipeline/changes/2026-11-05-projection-owner-shards.md">

<violation number="1" location="backend/docs/listen_pusher_pipeline/changes/2026-11-05-projection-owner-shards.md:21">
P2: The 30-page bound excludes the five-page tail and the finish page. State the total as up to 35 discovery pages plus one finish page, or reduce the component budgets to preserve the advertised read bound.</violation>

<violation number="2" location="backend/docs/listen_pusher_pipeline/changes/2026-11-05-projection-owner-shards.md:25">
P2: This heading overstates the guarantee: several non-terminal head refusal classes explicitly remain waiting, and the blocked-head deadline is disabled by default. Rename it to limit the exit guarantee to eligible failing or blocked heads.</violation>
</file>

<file name="backend/tests/unit/test_batch_projection_shard_drain.py">

<violation number="1" location="backend/tests/unit/test_batch_projection_shard_drain.py:35">
P2: This fixture does not force the shard prerequisites on, so an inherited false value makes `engaged` false and these tests stop exercising shard routing. Set all required shard/owner-chain switches to `true` (and keep prefetch separately unset) to make the tests environment-independent.</violation>
</file>

<file name="backend/docs/batch-pipeline-serving.md">

<violation number="1" location="backend/docs/batch-pipeline-serving.md:1265">
P3: `Each owner has one drain writer fleet-wide` is only true while its shard is held and Redis is healthy. Unheld shards and local fallback requests can drain on another instance.</violation>
</file>

<file name="backend/utils/batch_pipeline/projection_shard_pump.py">

<violation number="1" location="backend/utils/batch_pipeline/projection_shard_pump.py:129">
P2: Future-dated captures are admitted to the fresh lane and scored with their future timestamp, so clock-skewed audio can outrank genuinely current work. Reject future ages or clamp the capture score before assigning the fresh lane, matching the sync lane’s future-skew handling.</violation>

<violation number="2" location="backend/utils/batch_pipeline/projection_shard_pump.py:232">
P1: `free_slots` reaching zero stops the pump from admitting more owners, while productive turns can immediately requeue the same owner. Admit a bounded waiting set between continuations so other owners can reach the drain FIFO.</violation>

<violation number="3" location="backend/utils/batch_pipeline/projection_shard_pump.py:269">
P2: Deferred backlog wakes are still eligible before their retry time after a process restart because `pick` never checks the wake score. Filter future backlog scores here so a stuck owner cannot be retried immediately and monopolize a drain slot.</violation>
</file>

<file name="backend/config/batch_pipeline.py">

<violation number="1" location="backend/config/batch_pipeline.py:921">
P2: `BATCH_PROJECTION_SHARDS_ENABLED=1` or `on` does not enable shards in the normal serving mode, despite the documented `true/1/on` contract. Parse the documented truthy values before passing the legacy value to `rollout_default_off`.</violation>
</file>

<file name="backend/docs/listen_pusher_pipeline.mdx">

<violation number="1" location="backend/docs/listen_pusher_pipeline.mdx:9366">
P3: The documented enablement set omits `yes`, although the shared rollout parser accepts it. Add `yes` to the contract or narrow the parser so operators do not get a surprising rollout value.</violation>

<violation number="2" location="backend/docs/listen_pusher_pipeline.mdx:9397">
P2: Turn and step-budget continuations do not cool or defer their wakes; `OwnerDrains` immediately requeues them at the FIFO tail. Document cooldown only for blocked, error, or hold-not-landed stops, or change the continuation behavior.</violation>

<violation number="3" location="backend/docs/listen_pusher_pipeline.mdx:9420">
P2: The backstop can route the same owner twice per tick because tail and whole-log discovery use separate deduplication sets. Share a per-tick owner set or document the two-pass behavior; otherwise duplicate wake pushes add Redis work and advance wake sequences unnecessarily.</violation>

<violation number="4" location="backend/docs/listen_pusher_pipeline.mdx:9432">
P2: Wake pushes and routed drain requests call Redis synchronously outside `db_executor`, so they can occupy the caller's executor for the Redis timeout. Route these operations through `db_executor` or narrow this statement to the pump's tick and depth operations.</violation>
</file>

<file name="backend/utils/batch_pipeline/projection.py">

<violation number="1" location="backend/utils/batch_pipeline/projection.py:210">
P2: `blocked_head_type` collapses every message-based eligible refusal to `ProjectionBlocked:other`, so changing from a handoff refusal to a VAD or manifest refusal does not restart the deadline. Encode each allowlisted message in the episode token so the current refusal receives its own deadline.</violation>

<violation number="2" location="backend/utils/batch_pipeline/projection.py:564">
P2: `discover_tail` routes an owner from its first row, so an older capture can force a mixed owner into BACKLOG and hide a newer fresh capture. Aggregate each owner’s rows and choose the fresh/latest lane before routing.</violation>
</file>

<file name="backend/utils/batch_pipeline/projection_shards.py">

<violation number="1" location="backend/utils/batch_pipeline/projection_shards.py:141">
P2: A stuck owner present in both lanes is not fully deferred: the fresh wake increments the backlog sequence before the already-taken backlog wake is processed, so that second defer returns 0 and the old backlog score remains at the head. Defer both lane entries atomically, or avoid changing the target sequence until the existing backlog wake has been handled, so stuck owners cannot keep crowding take pages.</violation>

<violation number="2" location="backend/utils/batch_pipeline/projection_shards.py:209">
P2: Record the transition with the shared fallback helper; otherwise a Redis refresh failure switches this instance to the legacy sweep without incrementing `omi_fallback_total`.</violation>

<violation number="3" location="backend/utils/batch_pipeline/projection_shards.py:400">
P2: `settle()` and `defer()` do not mark the coordinator inactive when their Redis calls fail, so the process can remain engaged instead of returning to the legacy sweep. Mark the shard coordinator unhealthy on these errors, consistently with `take()`, before returning failure.</violation>
</file>

<file name="backend/config/flag_budget.json">

<violation number="1" location="backend/config/flag_budget.json:27">
P3: The budget metadata is stale: `as_of` remains `2026-10-29` while this change adds five names from 2026-11-05. Update `as_of` to the actual inventory date so the count has an accurate audit timestamp.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

return 0
drains = projector.ensure_drains()
free = drains.free_slots
if free <= 0:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: free_slots reaching zero stops the pump from admitting more owners, while productive turns can immediately requeue the same owner. Admit a bounded waiting set between continuations so other owners can reach the drain FIFO.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At backend/utils/batch_pipeline/projection_shard_pump.py, line 232:

<comment>`free_slots` reaching zero stops the pump from admitting more owners, while productive turns can immediately requeue the same owner. Admit a bounded waiting set between continuations so other owners can reach the drain FIFO.</comment>

<file context>
@@ -0,0 +1,294 @@
+            return 0
+        drains = projector.ensure_drains()
+        free = drains.free_slots
+        if free <= 0:
+            return 0
+        now = self.monotonic()
</file context>

from utils.batch_pipeline.scheduler import owner_key
from utils.batch_pipeline.serving import Serving

ENV = (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: These tests do not isolate all kill switches that control the behavior they assert. Clear BATCH_PIPELINE_OWNER_DRAIN_ENABLED and BATCH_PIPELINE_SERVING_PROJECTION_SWEEP in the autouse fixture (or set every required switch explicitly) so CI/deployment environment flags cannot change the test outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At backend/tests/unit/test_batch_projection_shards.py, line 23:

<comment>These tests do not isolate all kill switches that control the behavior they assert. Clear `BATCH_PIPELINE_OWNER_DRAIN_ENABLED` and `BATCH_PIPELINE_SERVING_PROJECTION_SWEEP` in the autouse fixture (or set every required switch explicitly) so CI/deployment environment flags cannot change the test outcome.</comment>

<file context>
@@ -0,0 +1,671 @@
+from utils.batch_pipeline.scheduler import owner_key
+from utils.batch_pipeline.serving import Serving
+
+ENV = (
+    'BATCH_PROJECTION_SHARDS_ENABLED',
+    'BATCH_PROJECTION_BLOCKED_HEAD_DEADLINE_ENABLED',
</file context>

3. **Fresh first.** Commits of audio captured under 6 h ago take the `fresh`
lane, newest capture first. Two drain slots per process stay reserved for
backlog while backlog waits. A stuck owner moves behind due owners.
4. **Every non-terminal head has an exit.**

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This heading overstates the guarantee: several non-terminal head refusal classes explicitly remain waiting, and the blocked-head deadline is disabled by default. Rename it to limit the exit guarantee to eligible failing or blocked heads.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At backend/docs/listen_pusher_pipeline/changes/2026-11-05-projection-owner-shards.md, line 25:

<comment>This heading overstates the guarantee: several non-terminal head refusal classes explicitly remain waiting, and the blocked-head deadline is disabled by default. Rename it to limit the exit guarantee to eligible failing or blocked heads.</comment>

<file context>
@@ -0,0 +1,49 @@
+3. **Fresh first.** Commits of audio captured under 6 h ago take the `fresh`
+   lane, newest capture first. Two drain slots per process stay reserved for
+   backlog while backlog waits. A stuck owner moves behind due owners.
+4. **Every non-terminal head has an exit.**
+   - Failing heads already took the held disposition after 24 h. That bound
+     is now `BATCH_PROJECTION_HEAD_DEADLINE_SECONDS`.
</file context>
Suggested change
4. **Every non-terminal head has an exit.**
4. **Eligible failing and blocked heads have an exit.**

monkeypatch.setattr(projection_drain, 'projection_claim_held', lambda namespace, key: key in state['held'])
monkeypatch.setattr(projection, 'projection_claim', claim)
monkeypatch.setattr(projection_followup, 'projection_claim', claim)
for name in ('BATCH_PIPELINE_OWNER_DRAIN_ENABLED', 'BATCH_PIPELINE_PROJECTION_PREFETCH'):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This fixture does not force the shard prerequisites on, so an inherited false value makes engaged false and these tests stop exercising shard routing. Set all required shard/owner-chain switches to true (and keep prefetch separately unset) to make the tests environment-independent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At backend/tests/unit/test_batch_projection_shard_drain.py, line 35:

<comment>This fixture does not force the shard prerequisites on, so an inherited false value makes `engaged` false and these tests stop exercising shard routing. Set all required shard/owner-chain switches to `true` (and keep prefetch separately unset) to make the tests environment-independent.</comment>

<file context>
@@ -0,0 +1,122 @@
+    monkeypatch.setattr(projection_drain, 'projection_claim_held', lambda namespace, key: key in state['held'])
+    monkeypatch.setattr(projection, 'projection_claim', claim)
+    monkeypatch.setattr(projection_followup, 'projection_claim', claim)
+    for name in ('BATCH_PIPELINE_OWNER_DRAIN_ENABLED', 'BATCH_PIPELINE_PROJECTION_PREFETCH'):
+        monkeypatch.delenv(name, raising=False)
+
</file context>

if not self.shards.covered(key):
return False
now = self.shards.clock()
if capture_at is not None and now - capture_at <= FRESH_CAPTURE_SECONDS:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Future-dated captures are admitted to the fresh lane and scored with their future timestamp, so clock-skewed audio can outrank genuinely current work. Reject future ages or clamp the capture score before assigning the fresh lane, matching the sync lane’s future-skew handling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At backend/utils/batch_pipeline/projection_shard_pump.py, line 129:

<comment>Future-dated captures are admitted to the fresh lane and scored with their future timestamp, so clock-skewed audio can outrank genuinely current work. Reject future ages or clamp the capture score before assigning the fresh lane, matching the sync lane’s future-skew handling.</comment>

<file context>
@@ -0,0 +1,294 @@
+        if not self.shards.covered(key):
+            return False
+        now = self.shards.clock()
+        if capture_at is not None and now - capture_at <= FRESH_CAPTURE_SECONDS:
+            lane, score = FRESH, float(capture_at)
+        else:
</file context>
Suggested change
if capture_at is not None and now - capture_at <= FRESH_CAPTURE_SECONDS:
if capture_at is not None and 0 <= now - capture_at <= FRESH_CAPTURE_SECONDS:

only if no push landed since the take. So a commit during a drain always gets
another drain.

A drain that stops short (blocked, error, turn or step budget, hold not

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Turn and step-budget continuations do not cool or defer their wakes; OwnerDrains immediately requeues them at the FIFO tail. Document cooldown only for blocked, error, or hold-not-landed stops, or change the continuation behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At backend/docs/listen_pusher_pipeline.mdx, line 9397:

<comment>Turn and step-budget continuations do not cool or defer their wakes; `OwnerDrains` immediately requeues them at the FIFO tail. Document cooldown only for blocked, error, or hold-not-landed stops, or change the continuation behavior.</comment>

<file context>
@@ -9351,3 +9351,144 @@ owner-drain admissions use a read-only Redis claim GET and fail open on errors.
+only if no push landed since the take. So a commit during a drain always gets
+another drain.
+
+A drain that stops short (blocked, error, turn or step budget, hold not
+landed) cools the owner for about 120 s in process memory. It also moves the
+wake to `backlog` at its retry time, so stuck owners never fill the take page
</file context>

self.members = 0
# False until a lease pass succeeds, and after any Redis error: the
# caller then runs the legacy sweep on this instance.
self.healthy = False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Record the transition with the shared fallback helper; otherwise a Redis refresh failure switches this instance to the legacy sweep without incrementing omi_fallback_total.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At backend/utils/batch_pipeline/projection_shards.py, line 209:

<comment>Record the transition with the shared fallback helper; otherwise a Redis refresh failure switches this instance to the legacy sweep without incrementing `omi_fallback_total`.</comment>

<file context>
@@ -0,0 +1,466 @@
+        self.members = 0
+        # False until a lease pass succeeds, and after any Redis error: the
+        # caller then runs the legacy sweep on this instance.
+        self.healthy = False
+        self.refreshed_at: float | None = None
+        self._lock = threading.Lock()
</file context>

### Owner shards and push wakes (dark, 2026-11-05)

`BATCH_PROJECTION_SHARDS_ENABLED` (default off) replaces the per-instance
sweep's owner discovery with owner-hash shard leases. Each owner has one

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Each owner has one drain writer fleet-wide is only true while its shard is held and Redis is healthy. Unheld shards and local fallback requests can drain on another instance.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At backend/docs/batch-pipeline-serving.md, line 1265:

<comment>`Each owner has one drain writer fleet-wide` is only true while its shard is held and Redis is healthy. Unheld shards and local fallback requests can drain on another instance.</comment>

<file context>
@@ -1259,6 +1259,22 @@ below have their own switches; all three off is the pre-drain behaviour):
+### Owner shards and push wakes (dark, 2026-11-05)
+
+`BATCH_PROJECTION_SHARDS_ENABLED` (default off) replaces the per-instance
+sweep's owner discovery with owner-hash shard leases. Each owner has one
+drain writer fleet-wide, and that writer is woken by merge commits rather than
+by a polling keyset.
</file context>


| Env | Default | Bounds and fallback |
| --- | --- | --- |
| `BATCH_PROJECTION_SHARDS_ENABLED` | off | Only `true`/`1`/`on` enables it. Off is the per-instance sweep. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The documented enablement set omits yes, although the shared rollout parser accepts it. Add yes to the contract or narrow the parser so operators do not get a surprising rollout value.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At backend/docs/listen_pusher_pipeline.mdx, line 9366:

<comment>The documented enablement set omits `yes`, although the shared rollout parser accepts it. Add `yes` to the contract or narrow the parser so operators do not get a surprising rollout value.</comment>

<file context>
@@ -9351,3 +9351,144 @@ owner-drain admissions use a read-only Redis claim GET and fail open on errors.
+
+| Env | Default | Bounds and fallback |
+| --- | --- | --- |
+| `BATCH_PROJECTION_SHARDS_ENABLED` | off | Only `true`/`1`/`on` enables it. Off is the per-instance sweep. |
+| `BATCH_PROJECTION_SHARD_MEMBER` | true | `false` keeps the process out of shard leases. It still routes and pushes wakes. |
+| `BATCH_PROJECTION_BACKLOG_RESERVE` | 2 | 0..16, never more than the drain slots (`BATCH_PIPELINE_MAX_OWNER_DRAINS`). Malformed values keep 2. |
</file context>
Suggested change
| `BATCH_PROJECTION_SHARDS_ENABLED` | off | Only `true`/`1`/`on` enables it. Off is the per-instance sweep. |
| `BATCH_PROJECTION_SHARDS_ENABLED` | off | Only `true`/`1`/`on`/`yes` enables it. Off is the per-instance sweep. |

"seed_review_by": "2026-11-15",
"budget": 169,
"count": 259,
"count": 264,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The budget metadata is stale: as_of remains 2026-10-29 while this change adds five names from 2026-11-05. Update as_of to the actual inventory date so the count has an accurate audit timestamp.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At backend/config/flag_budget.json, line 27:

<comment>The budget metadata is stale: `as_of` remains `2026-10-29` while this change adds five names from 2026-11-05. Update `as_of` to the actual inventory date so the count has an accurate audit timestamp.</comment>

<file context>
@@ -24,7 +24,7 @@
   "seed_review_by": "2026-11-15",
   "budget": 169,
-  "count": 259,
+  "count": 264,
   "names": {
     "BATCH_PIPELINE_SERVING_PROJECTION_SWEEP": {"owner": "omi-pipeline / lead2", "review_by": "2026-12-05", "registry": "config/feature-flags.yaml"},
</file context>

A member taken off the sharded path by a drain kill switch kept renewing
its leases, because tick() refreshed before the engagement gate. Other
members then routed wakes into lanes nobody read, and those owners stalled
silently. A switched-off member now releases its leases, leaves the
members set, and skips lease passes until the switches are back on. It
rejoins on the next pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017CeWy13948MM5eAUJE7zqo

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38305edb17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


fresh_ok = [w for w in fresh if eligible(w)]
backlog_ok = [w for w in backlog if eligible(w)]
reserve = min(projection_backlog_reserve(), free) if backlog_ok else 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Count active backlog drains toward the reserved capacity

When owners finish one or two at a time while both lanes have waiting work, the default reserve of two assigns every newly available slot to backlog, even if other backlog owners already occupy the reserved capacity. After the initial fresh owners finish, all slots can become backlog slots, and fresh captures then starve until the backlog empties or at least three slots happen to become free together. Calculate the reservation against backlog drains already in flight rather than reserving two additional slots on every admission.

Useful? React with 👍 / 👎.

Comment on lines +1650 to +1652
and projection_blocked_head_deadline_enabled()
and blocked_head_deadline_eligible(error)
and self.resolve_blocked_head(segment_id, job, entry, binding, error)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply the blocked-head deadline to sweep-discovered heads

With the blocked-head deadline enabled but sharding disabled, an owner's sole pending result can repeatedly raise an eligible refusal without ever reaching this handler. sweep() catches ProjectionBlocked and continues without scheduling an owner drain or calling resolve_blocked_head(), so it never stamps projection_blocked_since and the result remains blocked indefinitely. This also affects dedicated projection-pump processes, which retain the plain sweep. Route eligible blocked heads into the walker, or apply the deadline from the sweep after verifying that the result is the chain head.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
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.

2 participants