Repository navigation
Prevent duplicate private bot retries after a resend #8651
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
loopx-agent
wants to merge
3
commits into
main
Choose a base branch
from
codex/hold-duplicate-bot-recovery
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+185
−16
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -186,6 +186,106 @@ def now(cls, tz=None): | |
| runtime.close() | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("lost_receipt", [False, True], ids=["accepted", "lost-outer-receipt"]) | ||
| def test_original_retry_yields_to_a_newer_equivalent_lark_request(ordinary, monkeypatch, lost_receipt): | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] 新 fixture 触发 Ruff F811。 新测试直接声明导入的 ordinary fixture,缺少本文件已有的局部 F811 标注。相同 Ruff 命令在原文件无诊断,新 head 在第 190 行报错。 沿用相邻测试的行级 # noqa: F811,保留普通 fixture 的名字和断言;勿关闭文件或项目级规则。 #8657 处理的是另一文件;对当前行添加同类标注后,独立 Ruff 对照通过。 |
||
| from datetime import datetime, timedelta, timezone | ||
|
|
||
| from loopx.capabilities.native_chat import external_conversations | ||
| from loopx.capabilities.native_chat.codex_adapter import CodexAppServerAdapter | ||
| from test_lark_private_conversations import connect | ||
|
|
||
| store, runtime, provider, transport = connect(ordinary) | ||
| fake = ordinary[-2] | ||
| fake.write_text(fake.read_text().replace('"codexErrorInfo": "cyberPolicy"', | ||
| '"codexErrorInfo": "usageLimitExceeded"').replace( | ||
| ' print(json.dumps({"method": "item/agentMessage/delta", "params": {"threadId": "durable-thread", "turnId": active_turn, "delta": "Partial answer."}}), flush=True)\n', '')) | ||
|
|
||
| def admit(profile, name, text): | ||
| event = provider.event(profile, name, text) | ||
| transport.admit(profile, event) | ||
| provider_ref = transport._binding(profile)["provider_ref"] | ||
| return transport.core.read_request(identity_ref(provider_ref, event["message_id"])) | ||
|
|
||
| text = "typed terminal failure" | ||
| try: | ||
| original = admit("notes-app", "quota-original", text) | ||
| sid, tid = original["session_id"], original["turn_id"] | ||
| failed = runtime.wait_for_turn(session_id=sid, turn_id=tid, timeout_sec=10) | ||
| assert failed["error_code"] == "usage_limit_exceeded" and failed["delta_count"] == 0 | ||
| transport.reconcile() | ||
| original = transport.core.read_request(original["request_ref"]) | ||
| failed_bytes = store._turn_path(sid, tid).read_bytes() | ||
| lark_failure_writes = list(provider.writes) | ||
| assert original["delivery_verified"] and lark_failure_writes | ||
|
|
||
| # Leave new source inputs in the real FIFO before they become native Turns. | ||
| monkeypatch.setattr(runtime, "resume_session_queue", lambda **_: None) | ||
| unrelated = admit("steward-app", "other-source", text) | ||
| distinct = admit("notes-app", "different-text", "Another request") | ||
| assert unrelated["session_id"] != sid and distinct["session_id"] == sid | ||
|
|
||
| class AfterBackoff(datetime): | ||
| @classmethod | ||
| def now(cls, tz=None): | ||
| return datetime.now(timezone.utc) + timedelta(seconds=61) | ||
|
|
||
| monkeypatch.setattr(external_conversations, "datetime", AfterBackoff) | ||
| monkeypatch.setattr(CodexAppServerAdapter, "failed_request_observation", staticmethod(lambda **_: { | ||
| "thread_id": "durable-thread", "turn_id": failed["upstream_turn_id"], | ||
| "only_original_user_item": True, "codex_error_info": "usageLimitExceeded", | ||
| "quota_available": True, "quota_observed_at": AfterBackoff.now().timestamp() - 1})) | ||
| assert transport.core.context_recovery_candidate(original["request_ref"]) | ||
|
|
||
| resend = provider.event("notes-app", "same-source-resend", text) | ||
| resend_ref = identity_ref(transport._binding("notes-app")["provider_ref"], resend["message_id"]) | ||
| resend_client = f"external-{resend_ref}" | ||
| if lost_receipt: | ||
| def fail_before_queue(**_): | ||
| raise OSError("Core queue unavailable") | ||
|
|
||
| with monkeypatch.context() as fault: | ||
| fault.setattr(runtime, "enqueue_turn", fail_before_queue) | ||
| with pytest.raises(OSError, match="Core queue unavailable"): | ||
| transport.admit("notes-app", resend) | ||
| assert transport.core.read_request(resend_ref)["session_id"] == sid | ||
| assert store.turn_for_client(sid, resend_client) is None | ||
| assert transport.core.context_recovery_candidate(original["request_ref"]) | ||
|
|
||
| write = external_conversations._atomic_write_json | ||
|
|
||
| def lose_outer_receipt(path, row, **kwargs): | ||
| if path.stem == resend_ref and row.get("status") == "accepted": | ||
| raise OSError("outer receipt unavailable") | ||
| write(path, row, **kwargs) | ||
|
|
||
| with monkeypatch.context() as fault: | ||
| fault.setattr(external_conversations, "_atomic_write_json", lose_outer_receipt) | ||
| with pytest.raises(OSError, match="outer receipt unavailable"): | ||
| transport.admit("notes-app", resend) | ||
| prepared = transport.core.read_request(resend_ref) | ||
| assert prepared["status"] == "prepared" and prepared["turn_id"] is None | ||
| else: | ||
| transport.admit("notes-app", resend) | ||
| assert transport.core.read_request(resend_ref)["status"] == "accepted" | ||
|
|
||
| resent = store.turn_for_client(sid, resend_client) | ||
| assert resent["session_id"] == sid and resent["status"] == "queued" | ||
| assert any(item["kind"] == "turn.queued" for item in store.events_after(sid, resent["turn_id"], None)) | ||
| expected_queue = {distinct["turn_id"], resent["turn_id"]} | ||
| assert {row["turn_id"] for row in store.queued_turns(sid)} == expected_queue | ||
| transport.reconcile_request(transport.root / f"{original['request_ref']}.json") | ||
| assert transport.core.read_request(original["request_ref"]) == original | ||
| assert not transport.core.context_recovery_candidate(original["request_ref"]) | ||
| assert {row["turn_id"] for row in store.queued_turns(sid)} == expected_queue | ||
| assert store._turn_path(sid, tid).read_bytes() == failed_bytes | ||
| assert provider.writes == lark_failure_writes | ||
| transport.admit("notes-app", resend) | ||
| assert transport.core.read_request(resend_ref)["turn_id"] == resent["turn_id"] | ||
| assert {row["turn_id"] for row in store.queued_turns(sid)} == expected_queue | ||
| finally: | ||
| runtime.close() | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def recorded_return_continuation(private_return, monkeypatch): # noqa: F811 | ||
| """Use the real admission/recovery owners; only model/provider IO is fake.""" | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] 保留扫描测试的两条独立输入。 扫描测试用同一来源和 Session 的两条相同文字证明读取只做一遍。后输入现在合法抑制前一条,原测试仍要求两条都是候选,在进入共享读断言前失败。 给共享读测试的第二条历史记录换成不同文字,保留两条候选、每轮一次读和零 Provider 调用断言;继续单独验证同文只保留新候选。 基准通过,当前 head 失败;当前 head 的不同文本双候选与同文本单候选对照都通过。