- The `one_turn_restore["run_generation"]` stamp had no reader once settlement
moved to the invalidate chokepoint (the finalizer guards on its own generation
via _is_session_run_current); delete it and the test lines that set it.
- release-slot-then-evict-cached-agent (#44212 rationale) was duplicated in
/stop and eviction; one _drop_turn_slot owns it.
- Best-effort interrupt uses the repo's _log_suppressed seam like run_shutdown.
- turn_lease.rebind resolves the lease via token.lease like release does
(identity, not a session_id lookup); _held_turn_lease hands back the token
map so release/rebind stop re-peeking session state.
- Test trims: vacuous isinstance, registry-internals asserts.
Every `TurnLeaseToken` is now constructed by `SessionTurnLeaseRegistry.acquire`
with its concrete `_SessionLease`, so `release()` no longer needs the
`getattr(token, "lease", None) or self._leases.get(...)` fallback that #107013
left in place. Make `lease` a required constructor argument and resolve the
lease from the token alone; the mapping lookup could only ever return the same
object (or a stale alias after rotation, which is exactly the case identity
release exists to avoid).
Re-applies the gateway compat removal byte-for-byte; see 92d0bd0d73 for the
full inventory (30 re-exports/aliases + 2 shim modules dropped, 3 shim-only
names re-removed, 24 callers + 34 test files repointed). No new changes.
BASE exposed these public names; the simplify refactor dropped them (and deleted/removed their
tests) although plugins/connectors import them. Restore each with BASE's signature and body
(download() again routes its auth decision through is_relay_media_url), and restore the covering
tests ported to the new layout, plus DB-only/task-cwd coverage for remove_session/cleanup, the
zero->4096 chunking normalization for from_platform_entry, and len() on empty/populated registries.
A/B vs 63279301bc: /tmp/rf/rev/ab_publicapi.py identical output on both trees.
Follow-up to the #80376 salvage:
- TurnLeaseToken.degraded is dead code since acquire() started raising
TurnLeaseTimeoutError: the only constructor site passes the default and
repo-wide there are zero external readers. Remove the field, its ctor
param, the repr segment, and the always-False guards in rebind()/release();
the class docstring's 'retained for compatibility' claim described
consumers that do not exist.
- Revert pure black-rewrap churn in test_config_env_bridge_authority.py and
test_turn_lease.py (hunks on functions this change does not touch), keeping
the functional Windows-env/encoding additions.
- Turn the default-value test into an invariant: config default must equal
gateway.turn_lease.DEFAULT_LEASE_WAIT instead of pinning the 1800 literal.
- Rewrap the dangling 'Released' comment line in gateway/run.py.
Verified: 20/20 targeted gateway tests, ruff clean; mutation check — with
gateway/turn_lease.py reverted to pre-fix main the module fails, restored it
passes.
* fix(gateway): serialize concurrent turns per resolved session_id with a turn lease
Closes the serialization half of #64934. The busy guards are keyed by
routing key, but the durable transcript is owned by session_id — and
switch_session() makes the key→id mapping many-to-one (/resume from a
second chat/topic, CLI-continuity rebinding, async-delegation pinning,
topic-binding tip-walks). Two routing keys mapped to one session_id ran
concurrent turns on two different agent objects, invisible to every
per-key guard: flushes persisted in completion order, the identity-marker
dedup swallowed rows, and the second turn ran on a stale history base —
leaving a permanent user;user alternation wedge.
The fix: an asyncio lease keyed by RESOLVED session_id (gateway/turn_lease.py),
acquired in _handle_message_with_agent after session resolution is final
(post switch_session/tip-walk), immediately before the transcript load, and
released in _handle_message's finally on every exit path. Tokens are granted
per (routing key, run generation) so a stale unwind can never release a newer
turn's lease (#28686 ownership lesson). Same-key messages never reach the
acquisition point mid-turn (both routing-key guards hold them), so the lock
is uncontended outside the alias-key route — where the second turn now waits
for the first turn's flush and logs one WARNING naming the session and both
routing keys (pairs with the #67371 tripwire).
Fail-open: a stuck holder degrades to today's unserialized behavior with a
loud ERROR after agent.gateway_timeout — never a wedged session; a degraded
token holds nothing and can't steal the lease. Registry is size-capped and
never evicts a live lease. Persist-disabled review forks never dispatch
through _handle_message, so they cannot contend.
Known limits (tracked on #64934): CLI-continuity cross-process pairs need a
DB-level lease; mid-turn compression rotation leaves a small alias window
for a follow-up at the binding-sync sites.
Validation: 8 behavior tests (alias-key wait + flush order, no cross-session
contention, generation-scoped idempotent release, timeout fail-open without
lease theft, bounded registry, bare-runner-safe release wiring) + E2E against
a real SessionStore reproducing the issue's switch_session alias route —
strict alternation and arrival order preserved.
* refactor(gateway): conversation-scope funnel + mid-turn lease rebind
Completes the #64934 system beyond the point fix. Two structural changes,
both eliminating whole bug classes rather than instances:
1. _clear_conversation_scope — THE single conversation-boundary funnel.
/new, /resume, auto-reset, expiry finalization, and the
compression-exhausted reset each carried a hand-copied pop-list of the
per-session dicts, and the lists drifted every time a new dict was
added (#48031, #58403, #10702, #35809 were all 'boundary X forgot
dict Y' bugs). All five sites now make one funnel call driven by the
_CONVERSATION_SCOPED_STATE registry; adding a new conversation-scoped
dict means adding one name to the registry, and every boundary picks
it up automatically. Scope rules documented at the registry: turn-scoped
state, the monotonic generation counter, and the agent cache are
deliberately excluded (different lifecycles).
2. SessionTurnLeaseRegistry.rebind — the held turn lease now FOLLOWS
mid-turn compression rotation. Both rotation sites (session-hygiene
pre-compression, agent-result session_id swap) alias the same
_SessionLease object under the new id, so an alias routing key
resolving the fresh child (topic tip-walk) still serializes against
the in-flight turn. Closes the rotation-alias window flagged as a
known limit on #64934. Ownership-checked like release; when the
target id already has a live lease the rebind fails open with a loud
WARNING (never a mid-turn deadlock).
Tests: 3 new rebind behavior tests + 5 funnel behavior tests (including
a real-setter drift guard); the two AST change-detector pins in
test_10710/test_48031 were re-pointed at the funnel and the #58403 pin
converted to a behavioral test. E2E: rotation-alias scenario against a
real SessionStore + SessionDB — turn B on the fresh child waits behind
the rotated holder, sees its rows, alternation intact.