fix(tabularius): a fixed relay session id is frozen by the first client that claims it - #2003
fix(tabularius): a fixed relay session id is frozen by the first client that claims it#20034444J99 wants to merge 1 commit into
Conversation
…nt that claims it
Every serial dispatch receipt has 409'd at broker registration since 2026-07-19:
ConductError: conduct broker rejected request (409):
{"detail": "session_id is already registered to another identity"}
`register()` binds agent/surface/session_id from the authenticated principal and then
rejects any re-registration whose WHOLE identity object differs. Post-binding those three
are forced equal, so the only fields that can still differ are `provider_identity` and
`native_run_id` — both client-declared. That check is therefore structurally incapable of
catching an authority mismatch (`session_principals`, two lines later, is what guards
authority); it can only ever fire on cosmetic provider drift. With the fixed relay id
`dispatch-serial-results`, the first client to register owns the literal forever and every
later build takes a permanent 409.
This is the #1408 relay freeze, recurring on the fields the #1408 fix did not normalize.
Reproduced against the real broker with ONE principal throughout:
#1 provider_identity='limen-cli' : OK
#2 same again (idempotent re-register) : OK
#3 provider_identity='limen-cli-2' : CONFLICT
#4 'limen-cli-2' again (permanent?) : CONFLICT
#5 native_run_id set, provider unchanged : CONFLICT
#6 original identity still works : OK
The freeze is per-session-id-literal, which is why the symptom looked so specific: writers
using their own literals (`harvest`, `dispatch-async/reserve`, `heal-board/lifecycle-repair`)
kept landing receipts all month — the published projection holds 177 August receipts — while
the one path that records a *dispatch* was silently dead. `logs/throughput-governor.jsonl`
then read `dispatched: 0` on every pass and pinned the jules lane in `bootstrap` at 25/day,
making the requested 100/day structurally unreachable (#1995).
Fix: key the relay session id on exactly the identity fields the keeper compares but does not
normalize, so a drift opens a NEW session instead of conflicting. The digest covers whatever
is left unbound, so adding an identity field keys it automatically rather than re-opening the
freeze, and it is derived from stable per-build values (never per-process) so the keeper's
session table does not grow without bound.
Verified live: a real `limen dispatch --agent jules --limit 1 --live` now gets PAST
registration and reaches `submit()`. It stops there on a different and legitimate 409 —
`task ORG-artist-organ-face-0705 exact revision moved` — because the local projection has
been frozen at `track.date: 2026-07-26` since the board-publication PR rung was retired
(#2001 carries that). Two stacked defects; this commit is the first.
Refs #1995
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 41 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
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. Comment |
|
Withdrawn — cut from a 5-commit-stale base, so it silently reverted this file's |
There was a problem hiding this comment.
Pull request overview
Fixes a Tabularius relay-session registration conflict where a fixed relay session_id could be permanently “claimed” by the first client that registers it, causing subsequent relay attempts (notably serial dispatch receipts) to 409 and never record receipts.
Changes:
- Key the relay
session_idwith a short digest derived from the identity fields thatregister()compares but does not normalize, so provider/native-run drift opens a new session instead of conflicting. - Update compatibility-ticket registration to use the keyed relay
session_idrather than the bare literal. - Add regression tests covering the freeze scenario and validating the identity-key behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
cli/src/limen/tabularius.py |
Generates a keyed relay session id based on unbound identity fields to prevent permanent 409 conflicts on relay re-registration. |
cli/tests/test_tabularius.py |
Adds regression tests ensuring the relay session id is keyed and demonstrating that keyed ids avoid the provider-drift freeze. |
Suppressed comments (1)
cli/src/limen/tabularius.py:1157
_relay_identity_key()includes client-declarednative_run_id(and any future unbound fields) in the digest, so the docstring claim that it is derived from “stable per-build values, never per-process” is too absolute. If a caller sets a per-runnative_run_id, this intentionally creates new sessions and the table can grow; the docstring should reflect that constraint.
Keying the session id on that metadata turns a drift into a NEW session instead of a
conflict. The digest covers whatever is left unbound, so adding an identity field keys it
automatically rather than re-opening the freeze. It is derived from stable per-build
values, never per-process, so the keeper's session table does not grow without bound.
| # register() binds these three identity fields from the authenticated principal; every OTHER | ||
| # identity field is client-declared and is compared verbatim against the stored session. | ||
| _RELAY_PRINCIPAL_BOUND_IDENTITY_FIELDS = ("agent", "surface", "session_id") |
The first of the stacked defects behind #1995 / the 19-day jules stall.
Symptom
Every serial dispatch receipt has 409'd at broker registration since 2026-07-19. The jules
sessions launch for real; the receipt is never recorded, and the traceback lands in stdout that
the beat's sensor runner swallows (#1989), so nothing surfaced it for 19 days.
Root cause
register()bindsagent/surface/session_idfrom the authenticated principal, then rejectsany re-registration whose whole identity object differs (
identitiesEqualis astableStringifycompare on the worker;prior.identity != session.identityin the pythonbroker). Post-binding those three fields are forced equal — two come from the principal, one is
the lookup key — so the only fields that can still differ are
provider_identityandnative_run_id, both client-declared.That check is therefore structurally incapable of catching an authority mismatch. Authority is
guarded two lines later by
session_principals(session_id is already bound to another principal). The identity check can only ever fire on cosmetic provider drift — and with thefixed relay id
dispatch-serial-results, the first client to register owns that literal forever.This is the #1408 relay freeze, recurring on the fields the #1408 fix did not normalize —
_bind_conductor_identitycarries the comment describing exactly this failure mode.Reproduced against the real broker with one principal throughout:
provider_identitylimen-clilimen-cli(re-register)limen-cli-2limen-cli-2againlimen-cli+native_run_idlimen-cliWhy the symptom looked so narrow
The freeze is per-session-id-literal. Writers with their own literals (
harvest,dispatch-async/reserve,heal-board/lifecycle-repair, …) kept landing receipts all month — thepublished projection holds 177 August receipts — while the one path that records a dispatch
was dead. So the board kept moving and only the dispatch receipt vanished.
Downstream:
lane_throughput_windowcountsdispatch_logentries withstatus == "dispatched",found none, and
logs/throughput-governor.jsonlrecorded"dispatched": 0, "mode": "bootstrap", "cap": 25on every pass. The lane can never leave bootstrap, so the requested 100/day isstructurally unreachable (#1995).
Fix
Key the relay session id on exactly the identity fields the keeper compares but does not
normalize, so a drift opens a new session instead of conflicting. The digest is computed over
whatever is left unbound, so adding an identity field keys it automatically rather than re-opening
the freeze; and it derives from stable per-build values (never per-process), so the keeper's
session table does not grow without bound.
Verification
A real
limen dispatch --agent jules --limit 1 --liverun against the live keeper now getspast registration and reaches
submit(). It stops there on a different, legitimate 409 —task ORG-artist-organ-face-0705 exact revision moved— because the local projection has beenfrozen at
track.date: 2026-07-26ever since the board-publication PR rung was retired. That isthe second stacked defect and #2001 carries it.
3 regression tests added, incl. one that drives the real
ConductBrokerthrough the freeze andthen through the keyed id.
cli/tests/test_tabularius.py: 25 passed. Ruff clean.Non-deploy:
cli/**is not a deploy-trigger path (the api rail is dormant —GCP_SA_KEYexistsnowhere).
Refs #1995