[Core][Frontend] Unified Full-duplex Framework (#7413)

Signed-off-by: chickeyton <ngton2014@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
chickeytonandClaude Opus 5 authored and GitHub committed 2026-09-16 15:35:36 +08:00
1 parent 9005d78903
commit 99ff4f3070
182 files changed
+26951 -34743

No files matched your search

@@ -133,7 +133,7 @@ async def test_registry_failed_send_is_accepted_only_when_journaled(mocker, jour
assert accepted.call_args_list == expected_calls
raise RuntimeError("transport lost")
await registry.create("sid-send-failure", incarnation=0, send=failing_send, close=mocker.AsyncMock())
await registry.create("sid-send-failure", send=failing_send, close=mocker.AsyncMock())
with pytest.raises(RuntimeError, match="transport lost"):
await registry.send_event(
@@ -152,7 +152,7 @@ async def test_registry_detached_event_is_accepted_only_when_journaled(mocker, j
registry = DuplexSessionAttachmentRegistry(replay_ttl_s=60.0, replay_max_bytes_per_session=4096)
send = mocker.AsyncMock()
accepted = mocker.Mock()
created = await registry.create("sid-detached-acceptance", incarnation=0, send=send, close=mocker.AsyncMock())
created = await registry.create("sid-detached-acceptance", send=send, close=mocker.AsyncMock())
await registry.detach("sid-detached-acceptance", attachment_generation=created.attachment_generation)
entry = await registry.send_event(
@@ -175,7 +175,7 @@ async def test_registry_unjournaled_event_is_accepted_after_successful_send(mock
async def send(payload):
accepted.assert_not_called()
await registry.create("sid-live-acceptance", incarnation=0, send=send, close=mocker.AsyncMock())
await registry.create("sid-live-acceptance", send=send, close=mocker.AsyncMock())
entry = await registry.send_event(
"sid-live-acceptance",
@@ -193,7 +193,7 @@ async def test_registry_overflow_does_not_accept_or_send_event(mocker) -> None:
registry = DuplexSessionAttachmentRegistry(replay_ttl_s=60.0, replay_max_bytes_per_session=100)
accepted = mocker.Mock()
send = mocker.AsyncMock()
await registry.create("sid-overflow-acceptance", incarnation=0, send=send, close=mocker.AsyncMock())
await registry.create("sid-overflow-acceptance", send=send, close=mocker.AsyncMock())
with pytest.raises(DuplexJournalOverflowError, match="byte limit"):
await registry.send_event(
@@ -231,13 +231,12 @@ async def test_registry_resume_rotates_token_replays_and_atomically_replaces_att
async def close_b(reason):
closes_b.append(reason)
created = await registry.create("sid", incarnation=2, send=send_a, close=close_a)
created = await registry.create("sid", send=send_a, close=close_a)
await registry.send_event("sid", {"type": "event-1"})
await registry.send_event("sid", {"type": "event-2"})
resumed = await registry.resume(
"sid",
incarnation=2,
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=1,
send=send_b,
@@ -260,6 +259,34 @@ async def test_registry_resume_rotates_token_replays_and_atomically_replaces_att
assert closes_a == [] and sends_b == [] and closes_b == []
@pytest.mark.asyncio
async def test_registry_detach_without_a_generation_targets_the_current_attachment_once() -> None:
registry = DuplexSessionAttachmentRegistry(replay_ttl_s=60.0, replay_max_bytes_per_session=4096)
sends: list[dict] = []
async def send(payload):
sends.append(payload)
async def close(reason):
return None
await registry.create("sid-current", send=send, close=close)
# The outbound pump outlives any one connection and only knows the session:
# ``None`` means "whichever socket is attached right now".
assert await registry.detach("sid-current", attachment_generation=None) is True
# A second report of the same dead socket is not a new detach, so it cannot
# restart the engine's disconnect grace window.
assert await registry.detach("sid-current", attachment_generation=None) is False
assert await registry.detach("sid-current", attachment_generation=1) is False
assert await registry.is_current_attachment("sid-current", 1) is False
# A detached session still journals for a later resume.
await registry.send_event("sid-current", {"type": "event-1"})
assert sends == []
assert await registry.detach("missing-session", attachment_generation=None) is False
@pytest.mark.asyncio
async def test_registry_resume_sends_activation_then_replay_before_new_live_events() -> None:
registry = DuplexSessionAttachmentRegistry(
@@ -282,13 +309,12 @@ async def test_registry_resume_sends_activation_then_replay_before_new_live_even
activation_started.set()
await release_activation.wait()
created = await registry.create("sid-order", incarnation=0, send=old_send, close=close)
created = await registry.create("sid-order", send=old_send, close=close)
await registry.detach("sid-order", attachment_generation=1)
await registry.send_event("sid-order", {"type": "replayed"})
resume_task = asyncio.create_task(
registry.resume(
"sid-order",
incarnation=0,
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=new_send,
@@ -322,13 +348,12 @@ async def test_registry_resume_delivery_failure_keeps_one_shot_old_token_recover
async def close(reason):
del reason
created = await registry.create("sid-recovery", incarnation=0, send=send, close=close)
created = await registry.create("sid-recovery", send=send, close=close)
await registry.detach("sid-recovery", attachment_generation=1)
with pytest.raises(RuntimeError, match="transport lost"):
await registry.resume(
"sid-recovery",
incarnation=0,
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=failing_send,
@@ -342,7 +367,6 @@ async def test_registry_resume_delivery_failure_keeps_one_shot_old_token_recover
recovered = await registry.resume(
"sid-recovery",
incarnation=0,
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
@@ -353,7 +377,6 @@ async def test_registry_resume_delivery_failure_keeps_one_shot_old_token_recover
with pytest.raises(InvalidResumeTokenError):
await registry.resume(
"sid-recovery",
incarnation=0,
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
@@ -374,13 +397,12 @@ async def test_registry_concurrent_resume_allows_exactly_one_rotated_token_winne
async def close(reason):
del reason
created = await registry.create("sid-race", incarnation=0, send=send, close=close)
created = await registry.create("sid-race", send=send, close=close)
async def attempt():
try:
return await registry.resume(
"sid-race",
incarnation=0,
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
@@ -396,7 +418,7 @@ async def test_registry_concurrent_resume_allows_exactly_one_rotated_token_winne
@pytest.mark.asyncio
async def test_registry_keeps_sessions_journals_and_incarnations_isolated() -> None:
async def test_registry_keeps_sessions_journals_and_tokens_isolated() -> None:
registry = DuplexSessionAttachmentRegistry(
replay_ttl_s=60.0,
replay_max_bytes_per_session=4096,
@@ -408,8 +430,8 @@ async def test_registry_keeps_sessions_journals_and_incarnations_isolated() -> N
async def close(reason):
del reason
created_a = await registry.create("sid-a", incarnation=1, send=send, close=close)
created_b = await registry.create("sid-b", incarnation=4, send=send, close=close)
created_a = await registry.create("sid-a", send=send, close=close)
created_b = await registry.create("sid-b", send=send, close=close)
await registry.detach("sid-a", attachment_generation=1)
await registry.detach("sid-b", attachment_generation=1)
await registry.send_event("sid-a", {"type": "a-only"})
@@ -417,7 +439,6 @@ async def test_registry_keeps_sessions_journals_and_incarnations_isolated() -> N
resumed_a = await registry.resume(
"sid-a",
incarnation=1,
resume_token=created_a.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
@@ -425,7 +446,6 @@ async def test_registry_keeps_sessions_journals_and_incarnations_isolated() -> N
)
resumed_b = await registry.resume(
"sid-b",
incarnation=4,
resume_token=created_b.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
@@ -434,11 +454,10 @@ async def test_registry_keeps_sessions_journals_and_incarnations_isolated() -> N
assert [entry.payload["type"] for entry in resumed_a.replay_entries] == ["a-only"]
assert [entry.payload["type"] for entry in resumed_b.replay_entries] == ["b-only"]
with pytest.raises(ValueError, match="incarnation mismatch"):
with pytest.raises(InvalidResumeTokenError):
await registry.resume(
"sid-a",
incarnation=2,
resume_token=resumed_a.resume_token.plaintext,
resume_token=resumed_b.resume_token.plaintext,
last_received_server_event_seq=1,
send=send,
close=close,
@@ -458,7 +477,7 @@ async def test_registry_repr_never_contains_plaintext_tokens() -> None:
async def close(reason):
del reason
created = await registry.create("sid-repr", incarnation=0, send=send, close=close)
created = await registry.create("sid-repr", send=send, close=close)
await registry.detach("sid-repr", attachment_generation=1)
entry = await registry.send_event(
"sid-repr",
@@ -469,3 +488,67 @@ async def test_registry_repr_never_contains_plaintext_tokens() -> None:
assert created.resume_token.plaintext not in repr(created)
assert created.resume_token.plaintext not in repr(entry)
assert created.resume_token.plaintext not in repr(registry)
@pytest.mark.asyncio
@pytest.mark.parametrize("cancel_on", ["activation", "replay"])
async def test_registry_resume_cancelled_mid_delivery_rolls_back_like_a_failure(cancel_on: str) -> None:
"""Cancellation must take the same rollback as an ordinary delivery failure.
``CancelledError`` derives from ``BaseException``, so an ``except
Exception`` around the activation and replay sends let a cancelled resume
keep its attachment as the current generation while the token it rotated
past was already gone — the session left attached to an unusable transport
with no way back. Both send points are covered because either can be the
one that gets cancelled.
"""
registry = DuplexSessionAttachmentRegistry(replay_ttl_s=60.0, replay_max_bytes_per_session=4096)
async def send(payload):
del payload
async def close(reason):
del reason
created = await registry.create("sid-cancel", send=send, close=close)
# One journaled event, so the replay loop has something to send.
await registry.send_event("sid-cancel", {"type": "response.output_text.delta", "delta": "a"})
await registry.detach("sid-cancel", attachment_generation=1)
sends = 0
cancel_at = 1 if cancel_on == "activation" else 2
async def cancelling_send(payload):
del payload
nonlocal sends
sends += 1
if sends == cancel_at:
raise asyncio.CancelledError
with pytest.raises(asyncio.CancelledError):
await registry.resume(
"sid-cancel",
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=cancelling_send,
close=close,
activation_payload_factory=lambda token, generation: {
"type": "session.resumed",
"resume_token": token.plaintext,
"attachment_generation": generation,
},
)
assert sends == cancel_at, "the send that was cancelled is the one under test"
# The cancelled attachment must not remain current...
assert not await registry.has_attachment("sid-cancel")
# ...and the token the caller presented has to work again, or the session
# is unreachable: resume() had already rotated past it.
recovered = await registry.resume(
"sid-cancel",
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
close=close,
)
assert recovered.attachment_generation == 3