Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
5b588a2
fix(http-bridge): stabilize silent and clean-close recovery
Jul 17, 2026
e95fbf3
fix: avoid hard anchor for synthesized bridge turn state
Komzpa Jul 17, 2026
3172455
docs: add @shaqman as a contributor
Komzpa Jul 17, 2026
e16fd4b
fix: harden bridge retry circuit cleanup
Komzpa Jul 17, 2026
e174c65
fix: close clean-close retry races
Komzpa Jul 17, 2026
c616a9d
fix(http-bridge): address reconnect clean-close review
Komzpa Jul 17, 2026
89d7ac0
Handle idle bridge retry transport failures
Komzpa Jul 18, 2026
48e5965
chore(proxy): keep rebase validation green
Jul 18, 2026
4c5019a
fix(http-bridge): avoid rejected account on model fallback
Jul 20, 2026
4716189
fix(http-bridge): retire stale gate holders with active siblings
Jul 20, 2026
e10ec0f
fix(http-bridge): synchronize retry circuit state
Jul 20, 2026
c206f6e
fix(http-bridge): stabilize websocket receive failures
Jul 22, 2026
1dcb7ad
fix(http-bridge): preserve upstream rebase behavior
Jul 24, 2026
512b9cc
fix(db): converge deployed request log migration heads
Jul 24, 2026
45c6074
fix(proxy): briefly retry saturated hard affinity owners
Jul 24, 2026
04088d5
fix(api): restore multipart request helpers
Jul 25, 2026
a53a992
fix(http-bridge): preserve file owners during recovery
Jul 25, 2026
b90f53a
fix(http-bridge): close recovery race paths
Jul 25, 2026
e489bf8
fix(http-bridge): refresh durable retry circuits
Jul 25, 2026
69fc09c
fix(migrations): converge retry circuit history cleanly
Jul 25, 2026
9902f45
fix(http-bridge): clean up cancelled handoffs
Jul 25, 2026
0468d63
fix(http-bridge): bound handoff and circuit state
Jul 25, 2026
6731b9e
fix(http-bridge): settle cancelled clean-close retries
Jul 25, 2026
bde5d41
fix(http-bridge): retain sessions during handoff
Jul 25, 2026
f84b91d
fix(http-bridge): cap anchored stuck-gate wait at 2x retire threshold
cigro-manager Jul 20, 2026
0656dd4
fix(http-bridge): judge stuck-gate staleness by upstream activity, no…
cigro-manager Jul 20, 2026
83a7f0b
debug(http-bridge): dump pending-state verdict inputs when stuck watc…
cigro-manager Jul 20, 2026
b13ee71
fix(http-bridge): treat missing response.created as the stuck-gate we…
cigro-manager Jul 20, 2026
48e39fe
fix(http-bridge): prevent stuck pre-response watchdog starvation
Jul 25, 2026
4de32c1
fix(http-bridge): preserve reader handoff and migration head
Jul 26, 2026
da57604
fix(db): repair stamped request usage rollup heads
Jul 26, 2026
17d03a4
fix(db): keep rollup repair downgrade forward-only
Jul 26, 2026
fd65c65
fix(db): converge pending tool call and repair heads
Jul 28, 2026
c72eb8d
fix(http-bridge): preserve draining siblings and cancellation
Jul 28, 2026
c657e8c
fix(http-bridge): hold handoff guard through lease swap
Jul 28, 2026
f439f3f
fix(http-bridge): guard every reconnect handoff
Jul 28, 2026
b3fe8e9
fix(http-bridge): wait for guarded registry handoffs
Jul 28, 2026
00cd90e
fix(http-bridge): complete handoff guards before validation
Jul 28, 2026
36fa29f
fix(http-bridge): finalize guarded handoffs on commit failure
Jul 28, 2026
216315b
fix(http-bridge): retain old lease through release
Jul 28, 2026
d91d0cc
fix(http-bridge): finalize reconnect failures consistently
Jul 28, 2026
595187d
fix(http-bridge): finalize lease release failures
Jul 28, 2026
6f08bbf
fix(http-bridge): preserve fallback and continuity coverage
Jul 28, 2026
3affe4c
fix(http-bridge): select retry state before fallback checks
Jul 28, 2026
35377d1
fix(http-bridge): protect reconnect futures from waiter eviction
Jul 28, 2026
973450b
test(http-bridge): cover protected handoff timeouts
Jul 28, 2026
8e6cdee
fix(http-bridge): preserve reset lineage and cleanup
Jul 28, 2026
77fb5d6
test(http-bridge): cover cancelled owner lease release
Jul 28, 2026
7afa525
fix(http-bridge): clear durable circuit after lookup errors
Jul 28, 2026
c0c1491
fix(http-bridge): gate retry circuit on pre-response failures
Jul 28, 2026
ffdebde
fix(http-bridge): penalize deferred pre-response failures
Jul 28, 2026
bc502ae
fix(http-bridge): age stale requests by upstream activity
Jul 28, 2026
de59203
fix(retry-circuit): preserve merged backoff and purge expiry
Jul 28, 2026
4720b0a
fix(retry-circuit): release global lock during durable I/O
Jul 28, 2026
70f6288
fix(retry-circuit): defer durable writes and task cleanup
Jul 28, 2026
07a8abc
fix(http-bridge): finalize unexpected reconnect failures
Jul 28, 2026
5336886
fix(retry-circuit): ignore stale cooldown after reset
Jul 28, 2026
9648b88
fix(http-bridge): exclude handoffs from capacity and surface storage …
Jul 28, 2026
ac1352b
fix(retry-circuit): guard stale purges and backoff exponent
Jul 28, 2026
8750b6a
fix(proxy): track deferred cancellation drains
Jul 28, 2026
5ec3f83
fix(http-bridge): clear cooldown mode on upstream activity
Jul 28, 2026
740298f
fix(http-bridge): pass reader cleanup ownership
Jul 28, 2026
ef50b9c
fix(retry-circuit): guard concurrent resets
Jul 28, 2026
3ec63f5
fix(retry-circuit): clear unknown timestamp state
Jul 28, 2026
7a99615
fix(retry-circuit): ignore stale failure snapshots
Jul 28, 2026
a154978
fix(http-bridge): add safe continuity recovery bounds
shaqman Jul 29, 2026
4a5265d
fix(http-bridge): preserve focused safe-continuity test diff
shaqman Jul 29, 2026
fe42789
fix(http-bridge): publish deployed recovery path
shaqman Jul 29, 2026
8cdc0ad
fix(http-bridge): fail fast hard no-safe-replay cooldowns
shaqman Jul 29, 2026
5c0ddad
fix(http-bridge): fail fast before cooldown keepalive loop
shaqman Jul 29, 2026
93951b3
fix(http-bridge): reject cooldown-bound requests before submit
shaqman Jul 29, 2026
855a2b3
fix(http-bridge): address owner review and bound ack watchdog
Jul 29, 2026
fceba50
fix(http-bridge): close review cleanup races
Jul 29, 2026
bdf7809
fix(http-bridge): preserve local circuit on purge failure
Jul 29, 2026
dc90963
fix(http-bridge): close retry ownership races
Jul 29, 2026
edbdc53
fix(http-bridge): make ambiguous recovery replay-safe
Jul 30, 2026
d78db7d
fix(http-bridge): narrow durable recovery replay
Jul 30, 2026
ed2ad68
fix(http-bridge): fence recovery journal ownership
Jul 30, 2026
c395b1f
fix(http-bridge): fence recovery recording ownership
Jul 30, 2026
55c240f
fix(http-bridge): retain recovery owner fence on conflicts
Jul 30, 2026
e0e66bf
fix(http-bridge): fail closed on stale recovery owner
Jul 30, 2026
b006d7d
fix(http-bridge): close remaining recovery races
Jul 30, 2026
ba7d07f
fix(http-bridge): keep recovery alias guard scoped
Jul 30, 2026
50115e1
fix(http-bridge): close journal and circuit clear races
Jul 30, 2026
77e4a41
fix(http-bridge): claim durable replay before resend
Jul 30, 2026
cae7a34
fix(http-bridge): fence replay claims and transfer leases
Jul 30, 2026
77b9935
fix(http-bridge): settle terminal recovery outcomes
Jul 30, 2026
640a2ca
fix(http-bridge): fence active recovery attempts
Jul 30, 2026
b8f2f7f
fix(http-bridge): arm fresh recovery journal fence
Jul 30, 2026
2286667
fix(http-bridge): preserve recovery journal identity
Jul 30, 2026
e8fcbe5
fix(http-bridge): preserve recovery and timeout safety
Jul 30, 2026
8199d39
fix(http-bridge): complete durable recovery fencing
Jul 30, 2026
d43d7f1
fix(http-bridge): preserve journal and alias ownership
Jul 30, 2026
75855fa
fix(http-bridge): finish recovery settlement fencing
Jul 30, 2026
e78875d
fix(http-bridge): fence response-observed recovery attempts
Jul 30, 2026
e469ca5
fix(http-bridge): renew lease during settlement backoff
Jul 30, 2026
3bff314
fix(http-bridge): preserve origin settlement ownership
Jul 30, 2026
945205f
fix(http-bridge): retain settlement fencing on shutdown
Jul 30, 2026
3e66fbf
fix(http-bridge): drain origin settlement before shutdown
Jul 30, 2026
32970ec
fix(http-bridge): bound recovery settlement fanout
Jul 30, 2026
4d0390f
fix(shutdown): drain settlements before owner transition
Jul 30, 2026
c6aea46
docs(openspec): align idle watchdog contract
Jul 30, 2026
01b6889
fix(shutdown): retain child cancellation cleanup
Jul 30, 2026
b933e88
fix(http-bridge): deduplicate settlement retries
Jul 30, 2026
9459301
fix(http-bridge): finalize settlement ownership
Jul 30, 2026
c0c957a
fix(http-bridge): preserve shutdown settlement fences
Jul 30, 2026
e4fb1f4
fix(http-bridge): release settled recovery origins
Jul 30, 2026
9442982
fix(http-bridge): track deferred cancellation cleanup
Jul 30, 2026
6f86988
fix(http-bridge): fence stale recovery takeover
Jul 30, 2026
952d93f
fix(http-bridge): revalidate recovery journal before dispatch
Jul 30, 2026
6e3715c
Fix HTTP bridge retry and recovery stability
Jul 31, 2026
52f6342
Fix CI settings drift and websocket cleanup
Aug 1, 2026
1a333a2
Fix telemetry deadlines and circuit clock skew
Aug 1, 2026
ddc800c
Handle lagging retry circuit clocks with CAS
Aug 1, 2026
aee76e7
Rollback un-dispatched recovery claims
Aug 1, 2026
b1e73d7
Carry durable replay claims into retries
Aug 1, 2026
89ed8a0
Track recovery dispatch after upstream send
Aug 1, 2026
4689b79
Record stuck gate timeouts in retry circuits
Aug 1, 2026
5d45c2e
Preserve retry hints in streaming errors
Aug 2, 2026
d27f2e1
fix(ci): merge migration heads and keep proxy facade under budget
Aug 3, 2026
84568e8
test(db): accept merged migration head
Aug 3, 2026
ce81335
Merge branch 'main' into fix/http-bridge-clean-close-penalty
Soju06 Aug 4, 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
19 changes: 19 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,25 @@
"contributions": [
"code"
]
},
{
"login": "shaqman",
"name": "Syakur Rahman",
"avatar_url": "https://avatars.githubusercontent.com/u/1113851?v=4",
"profile": "http://expressthisout.com/",
"contributions": [
"code",
"test"
]
},
{
"login": "cigro-manager",
"name": "cigro-manager",
"avatar_url": "https://avatars.githubusercontent.com/u/219247995?v=4",
"profile": "https://github.com/cigro-manager",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/e
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alchemistkiv"><img src="https://avatars.githubusercontent.com/u/93935225?v=4?s=100" width="100px;" alt="alchemistkiv"/><br /><sub><b>alchemistkiv</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=alchemistkiv" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=alchemistkiv" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mhooooo"><img src="https://avatars.githubusercontent.com/u/142386428?v=4?s=100" width="100px;" alt="mhooooo"/><br /><sub><b>mhooooo</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=mhooooo" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=mhooooo" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/crowscc"><img src="https://avatars.githubusercontent.com/u/17800561?v=4?s=100" width="100px;" alt="crowscc"/><br /><sub><b>crowscc</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=crowscc" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://expressthisout.com/"><img src="https://avatars.githubusercontent.com/u/1113851?v=4?s=100" width="100px;" alt="Syakur Rahman"/><br /><sub><b>Syakur Rahman</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=shaqman" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=shaqman" title="Tests">⚠️</a></td>
</tr>
</tbody>
</table>
Expand Down
2 changes: 2 additions & 0 deletions app/core/clients/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ def __init__(
upstream_status_code: int | None = None,
upstream_error_code: str | None = None,
failed_session: aiohttp.ClientSession | None = None,
retry_after_seconds: int | None = None,
) -> None:
super().__init__(f"Proxy response error ({status_code})")
self.status_code = status_code
Expand All @@ -488,6 +489,7 @@ def __init__(
self.upstream_status_code = upstream_status_code
self.upstream_error_code = upstream_error_code
self.failed_session = failed_session
self.retry_after_seconds = retry_after_seconds


def _process_network_failure_error(
Expand Down
37 changes: 35 additions & 2 deletions app/core/clients/proxy_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,24 @@ def normalize_realtime_call_id(value: str) -> str | None:
return normalized.lower()


def _consume_connection_lost_exception(done: asyncio.Future[Any]) -> None:
"""Retrieve close exceptions before websockets shields the waiter.

websockets 16 waits on ``connection_lost_waiter`` through
``asyncio.shield`` while completing ``ClientConnection.recv``. A peer
keepalive/protocol close therefore leaves an exception on the waiter,
which asyncio reports as an ``exception in shielded future`` even though
``recv`` translates it into an ``UpstreamWebSocketMessage``. Consume it
at the adapter boundary; ``receive`` still classifies the close normally.
"""
if done.cancelled():
return
try:
done.exception()
except asyncio.CancelledError:
return


@dataclass(slots=True)
class UpstreamWebSocketMessage:
kind: str
Expand Down Expand Up @@ -170,7 +188,14 @@ def _relay_receive_error_code(error_code: str) -> str | None:

# Relay owners map an absent code to their established stream_incomplete
# contract. Leaking the adapter's generic fallback would bypass that path.
return error_code if error_code == PROCESS_NETWORK_UNAVAILABLE_CODE else None
return error_code if error_code in {PROCESS_NETWORK_UNAVAILABLE_CODE, "upstream_keepalive_timeout"} else None


def _is_keepalive_timeout_close(exc: ConnectionClosedError) -> bool:
"""Classify peer/proxy heartbeat failures without exposing socket details."""

reason = _close_reason_from_exception(exc)
return "keepalive ping timeout" in f"{exc} {reason or ''}".lower()


async def _rotate_after_websocket_network_failure(error_code: str) -> None:
Expand Down Expand Up @@ -223,6 +248,9 @@ def __init__(
self._connection = connection
self._uses_proxy = uses_proxy
self._preserve_close_semantics = preserve_close_semantics
connection_lost_waiter = getattr(connection, "connection_lost_waiter", None)
if isinstance(connection_lost_waiter, asyncio.Future):
connection_lost_waiter.add_done_callback(_consume_connection_lost_exception)

async def send_text(self, text: str) -> None:
try:
Expand Down Expand Up @@ -254,6 +282,11 @@ async def receive(self) -> UpstreamWebSocketMessage:
)
error_code = _websocket_transport_error_code(exc, uses_proxy=self._uses_proxy)
await _rotate_after_websocket_network_failure(error_code)
relay_error_code = (
"upstream_keepalive_timeout"
if _is_keepalive_timeout_close(exc)
else _relay_receive_error_code(error_code)
)
# ConnectionClosedError describes an incomplete close handshake,
# not generic transport provenance. Let Responses relay owners map
# it to stream_incomplete while live relays preserve received closes.
Expand All @@ -265,7 +298,7 @@ async def receive(self) -> UpstreamWebSocketMessage:
if self._preserve_close_semantics
else str(exc)
),
error_code=_relay_receive_error_code(error_code),
error_code=relay_error_code,
)
except Exception as exc:
error_code = _websocket_transport_error_code(exc, uses_proxy=self._uses_proxy)
Expand Down
5 changes: 5 additions & 0 deletions app/core/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ class Settings(BaseSettings):
http_responses_session_bridge_stuck_gate_retire_after_seconds: float = Field(default=300.0, gt=0)
http_responses_session_bridge_max_sessions: int = Field(default=256, gt=0)
http_responses_session_bridge_queue_limit: int = Field(default=8, gt=0)
http_responses_session_bridge_clean_close_retry_jitter_max_seconds: float = Field(
Comment thread
shaqman marked this conversation as resolved.
default=2.0,
ge=0,
le=30.0,
)
http_responses_session_bridge_gateway_safe_mode: bool = False
http_responses_session_bridge_instance_id: str = Field(default_factory=_default_http_bridge_instance_id)
http_responses_session_bridge_instance_ring: Annotated[list[str], NoDecode] = Field(default_factory=list)
Expand Down
40 changes: 40 additions & 0 deletions app/core/metrics/prometheus.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,18 @@ def labels(self, *args: str, **kwargs: str) -> "HistogramLike": ...
["strength"],
registry=REGISTRY,
)
bridge_handoff_compatibility_rejection_total = Counter(
"codex_lb_bridge_handoff_compatibility_rejection_total",
"Total closed HTTP bridge admission handoffs rejected for incompatible request context",
["continuity_anchor", "preferred_account", "service_tier", "api_key_scope"],
registry=REGISTRY,
)
bridge_unanchored_handoff_recovery_total = Counter(
"codex_lb_bridge_unanchored_handoff_recovery_total",
"Total stale closed HTTP bridge admission handoffs recovered without a continuity anchor",
["reason"],
registry=REGISTRY,
)
bridge_local_rebind_total = Counter(
"codex_lb_bridge_local_rebind_total",
"Total bridge local rebinds by reason",
Expand Down Expand Up @@ -264,6 +276,24 @@ def labels(self, *args: str, **kwargs: str) -> "HistogramLike": ...
["reason", "affinity_kind", "model_class"],
registry=REGISTRY,
)
http_bridge_retry_circuit_total = Counter(
"codex_lb_http_bridge_retry_circuit_total",
"Total HTTP bridge automatic retry circuit outcomes",
["outcome"],
registry=REGISTRY,
)
stream_keepalive_sent_total = Counter(
"codex_lb_stream_keepalive_sent_total",
"Total downstream SSE keepalive frames emitted by surface",
["surface"],
registry=REGISTRY,
)
stream_idle_timeout_total = Counter(
"codex_lb_stream_idle_timeout_total",
"Total streams terminated after exceeding the configured idle window",
["surface"],
registry=REGISTRY,
)
cache_invalidation_bump_failures_total = Counter(
"codex_lb_cache_invalidation_bump_failures_total",
"Total cache invalidation version bumps that failed after retries",
Expand Down Expand Up @@ -315,6 +345,8 @@ def mark_process_dead() -> None:
bridge_first_turn_timeout_total: CounterLike | None = None
bridge_drain_recovery_allowed_total: CounterLike | None = None
bridge_owner_mismatch_total: CounterLike | None = None
bridge_handoff_compatibility_rejection_total: CounterLike | None = None
bridge_unanchored_handoff_recovery_total: CounterLike | None = None
bridge_local_rebind_total: CounterLike | None = None
bridge_forward_latency_seconds: HistogramLike | None = None
bridge_public_contract_error_total: CounterLike | None = None
Expand All @@ -329,6 +361,9 @@ def mark_process_dead() -> None:
proxy_phase_latency_seconds: HistogramLike | None = None
http_bridge_prewarm_total: CounterLike | None = None
http_bridge_stuck_retire_total: CounterLike | None = None
http_bridge_retry_circuit_total: CounterLike | None = None
stream_keepalive_sent_total: CounterLike | None = None
stream_idle_timeout_total: CounterLike | None = None
cache_invalidation_bump_failures_total: CounterLike | None = None
cache_invalidation_poll_failures_total: CounterLike | None = None

Expand All @@ -355,6 +390,7 @@ def mark_process_dead() -> None:
"bridge_durable_recover_total",
"bridge_drain_recovery_allowed_total",
"bridge_first_turn_timeout_total",
"bridge_handoff_compatibility_rejection_total",
"bridge_local_rebind_total",
"bridge_owner_forward_total",
"bridge_owner_mismatch_total",
Expand All @@ -363,14 +399,18 @@ def mark_process_dead() -> None:
"bridge_reattach_total",
"bridge_same_account_takeover_total",
"bridge_soft_local_rebind_total",
"bridge_unanchored_handoff_recovery_total",
"cache_invalidation_bump_failures_total",
"cache_invalidation_poll_failures_total",
"cap_partition_replicas",
"circuit_breaker_state",
"continuity_fail_closed_total",
"continuity_owner_resolution_total",
"http_bridge_prewarm_total",
"http_bridge_retry_circuit_total",
"http_bridge_stuck_retire_total",
"stream_keepalive_sent_total",
"stream_idle_timeout_total",
"image_request_duration_seconds",
"image_requests_total",
"make_scrape_registry",
Expand Down
5 changes: 4 additions & 1 deletion app/core/utils/sse.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import asyncio
import json
import re
from collections.abc import AsyncIterator, Mapping
from collections.abc import AsyncIterator, Callable, Mapping

from app.core.errors import ResponseFailedEvent
from app.core.types import JsonValue
Expand All @@ -26,6 +26,7 @@ async def inject_sse_keepalives(
interval_seconds: float,
*,
keepalive_frame: str = SSE_KEEPALIVE_FRAME,
on_keepalive: Callable[[], None] | None = None,
) -> AsyncIterator[str]:
"""Wrap an SSE event iterator and emit comment heartbeats on idle gaps.

Expand Down Expand Up @@ -56,6 +57,8 @@ async def _next_chunk(it: AsyncIterator[str]) -> str:
timeout=interval_seconds,
)
except asyncio.TimeoutError:
if on_keepalive is not None:
on_keepalive()
yield keepalive_frame
continue
except StopAsyncIteration:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""add durable HTTP bridge retry circuit state

Revision ID: 20260717_000000_add_http_bridge_retry_circuits
Revises: 20260717_000000_optimize_dashboard_hot_path_indexes
Create Date: 2026-07-17
"""

from __future__ import annotations

import sqlalchemy as sa
from alembic import op

revision = "20260717_000000_add_http_bridge_retry_circuits"
down_revision = "20260717_000000_optimize_dashboard_hot_path_indexes"
Comment thread
shaqman marked this conversation as resolved.
branch_labels = None
depends_on = None

_TABLE_NAME = "http_bridge_retry_circuits"


def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
if inspector.has_table(_TABLE_NAME):
return
op.create_table(
_TABLE_NAME,
Comment thread
Komzpa marked this conversation as resolved.
sa.Column("session_key_kind", sa.String(length=64), nullable=False),
sa.Column("session_key_hash", sa.String(length=64), nullable=False),
sa.Column("api_key_scope", sa.String(length=255), nullable=False),
sa.Column("consecutive_failures", sa.Integer(), nullable=False, server_default="0"),
sa.Column("cooldown_until_epoch", sa.Float(), nullable=False, server_default="0"),
sa.Column("last_detail", sa.String(length=255), nullable=True),
sa.Column("updated_at_epoch", sa.Float(), nullable=False),
sa.PrimaryKeyConstraint(
"session_key_kind",
"session_key_hash",
"api_key_scope",
name="pk_http_bridge_retry_circuits",
),
)


def downgrade() -> None:
bind = op.get_bind()
if sa.inspect(bind).has_table(_TABLE_NAME):
op.drop_table(_TABLE_NAME)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""merge retry circuits and dashboard index heads

Revision ID: 20260717_000001_merge_retry_circuits_and_dashboard_indexes
Revises:
- 20260717_000000_add_http_bridge_retry_circuits
Create Date: 2026-07-17 00:00:00.000000
"""

from __future__ import annotations

revision = "20260717_000001_merge_retry_circuits_and_dashboard_indexes"
down_revision = ("20260717_000000_add_http_bridge_retry_circuits",)
branch_labels = None
depends_on = None


def upgrade() -> None:
pass


def downgrade() -> None:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""merge the deployed retry-circuit and request-log schema heads

Revision ID: 20260724_000000_merge_request_log_schema_heads
Revises:
- 20260717_000001_merge_retry_circuits_and_dashboard_indexes
- 20260722_000000_backfill_request_log_useragent_families
Create Date: 2026-07-24 00:00:00.000000

The deployed SQLite database was previously stamped at the retry-circuit
merge revision while the request-log conversation-id branch was not applied.
Keeping the retry-circuit merge and the request-usage rollup as parents lets
Alembic apply the missing request-log revisions before converging on one head
without leaving the rollup revision as a second head or rewriting migration
history.
"""

from __future__ import annotations

revision = "20260724_000000_merge_request_log_schema_heads"
down_revision = (
"20260717_000001_merge_retry_circuits_and_dashboard_indexes",
"20260724_000000_add_request_usage_time_rollups",
)
branch_labels = None
depends_on = None


def upgrade() -> None:
pass


def downgrade() -> None:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""repair request-usage rollups for databases stamped at the old merge head

Revision ID: 20260726_000000_repair_request_usage_rollups_after_merge
Revises: 20260724_000000_merge_request_log_schema_heads
Create Date: 2026-07-26

Some deployed databases were stamped at the request-log merge revision before
the request-usage rollup child was connected to that merge. Changing the
parent tuple cannot make Alembic replay an already-applied revision, so those
databases need a forward-only repair step. The canonical rollup migration is
idempotent and safely creates any missing tables or watermark column here.
"""

from __future__ import annotations

import importlib
from types import ModuleType

revision = "20260726_000000_repair_request_usage_rollups_after_merge"
down_revision = "20260724_000000_merge_request_log_schema_heads"
Comment thread
shaqman marked this conversation as resolved.
branch_labels = None
depends_on = None


def _rollup_migration() -> ModuleType:
return importlib.import_module("app.db.alembic.versions.20260724_000000_add_request_usage_time_rollups")


def upgrade() -> None:
_rollup_migration().upgrade()


def downgrade() -> None:
# This revision repairs databases that were already stamped at the merge
# head. It must never remove objects owned by the canonical rollup
# revision, which remains an ancestor of that merge head on fresh installs.
pass
Loading
Loading