Skip to content

test(agent-server): cover conversation reads not serializing behind an unrelated start - #4685

Merged
VascoSch92 merged 2 commits into
mainfrom
vasco/test-4675-conversation-read-not-serialized
Aug 27, 2026
Merged

test(agent-server): cover conversation reads not serializing behind an unrelated start#4685
VascoSch92 merged 2 commits into
mainfrom
vasco/test-4675-conversation-read-not-serialized

Conversation

@VascoSch92

@VascoSch92 VascoSch92 commented Aug 27, 2026

Copy link
Copy Markdown
Member

HUMAN:

Just adding a regression test.


AGENT:

Why

#4675 reports that ConversationService._lifecycle_lock is a single process-wide
asyncio.Lock held on the read path of every conversation-scoped request, so any
request for conversation A queues behind a start/fork/delete/eviction for an
unrelated conversation B.

That code fix already landed in #4570 (replace global _lifecycle_lock with per-conversation locks, merged 2026-08-23) — four days before #4675 was filed.
The issue quotes conversation_service.py:647 and a _get_or_load_event_service
that opens with async with self._lifecycle_lock:; that is the pre-#4570 file.
On main all nine lifecycle sites already go through _conversation_lifecycle
(per-conversation) or _exclusive_lifecycle (registry-wide sweeps only).

What is not covered is the issue's acceptance criterion. The tests #4570 added
(test_conversation_lifecycle_serializes_only_matching_ids,
test_prepare_for_sandbox_pause_blocks_new_hydration) exercise the
_conversation_lifecycle helper and internals directly. Nothing drives the public
API, so the property callers actually depend on — a conversation-scoped read
completes while another conversation is mid-start
— can regress without any test
going red. This PR closes that gap rather than re-fixing solved code.

Summary

  • Add test_conversation_read_completes_while_another_conversation_starts to
    tests/agent_server/test_conversation_service.py: wedge conversation B inside
    _start_event_service, then assert get_event_service() for an already-live
    conversation A still resolves.
  • Drives the public API (start_conversation + get_event_service) instead of the
    lock helper, so it fails if the read path is ever put back behind a process-wide
    lock — regardless of how the locking is implemented internally.
  • No production code changes.

Issue Number

Fixes #4675

How to Test

The test is only meaningful if it fails without the fix, so it was verified in both
directions using two worktrees of this repo.

1 · It passes on main (fix present):

$ uv run --frozen pytest tests/agent_server/test_conversation_service.py -q --no-cov
113 passed, 375 warnings in 8.24s

2 · It reproduces the bug on 611629e69 — the commit immediately before #4570,
i.e. the exact code #4675 quotes. The same test body was dropped into a worktree at
that commit:

$ git worktree add --detach ../sdk-prefix 942114959^     # -> 611629e69
$ grep -c "_lifecycle_lock" openhands-agent-server/openhands/agent_server/conversation_service.py
10                                                        # process-wide lock, 9 hold sites
$ uv run --frozen pytest tests/agent_server/test_issue4675_repro.py -q --no-cov
FAILED tests/agent_server/test_issue4675_repro.py::test_conversation_read_completes_while_another_conversation_starts - TimeoutError
1 failed, 12 warnings in 5.52s

The TimeoutError is exactly the reported behaviour: get_event_service(A) never
returns while B sits inside _start_event_service holding the process-wide lock.

3 · Lint/type:

$ uv run --frozen pre-commit run --files tests/agent_server/test_conversation_service.py
Ruff format ......... Passed
Ruff lint ........... Passed
PEP8 (pycodestyle) .. Passed
Type check (pyright)  Passed

Video/Screenshots

Not applicable — no user-facing surface. The pass/fail transcripts above are the
evidence; the failing run against 611629e69 is what a screenshot would show.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Test-only change. The bug fix itself shipped in #4570.

Notes


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:9523b2e-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-9523b2e-python \
  ghcr.io/openhands/agent-server:9523b2e-python

All tags pushed for this build

ghcr.io/openhands/agent-server:9523b2e-golang-amd64
ghcr.io/openhands/agent-server:9523b2e127e2847b9bb8ba985a9c263229c6b426-golang-amd64
ghcr.io/openhands/agent-server:vasco-test-4675-conversation-read-not-serialized-golang-amd64
ghcr.io/openhands/agent-server:9523b2e-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:9523b2e-golang-arm64
ghcr.io/openhands/agent-server:9523b2e127e2847b9bb8ba985a9c263229c6b426-golang-arm64
ghcr.io/openhands/agent-server:vasco-test-4675-conversation-read-not-serialized-golang-arm64
ghcr.io/openhands/agent-server:9523b2e-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:9523b2e-java-amd64
ghcr.io/openhands/agent-server:9523b2e127e2847b9bb8ba985a9c263229c6b426-java-amd64
ghcr.io/openhands/agent-server:vasco-test-4675-conversation-read-not-serialized-java-amd64
ghcr.io/openhands/agent-server:9523b2e-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:9523b2e-java-arm64
ghcr.io/openhands/agent-server:9523b2e127e2847b9bb8ba985a9c263229c6b426-java-arm64
ghcr.io/openhands/agent-server:vasco-test-4675-conversation-read-not-serialized-java-arm64
ghcr.io/openhands/agent-server:9523b2e-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:9523b2e-python-amd64
ghcr.io/openhands/agent-server:9523b2e127e2847b9bb8ba985a9c263229c6b426-python-amd64
ghcr.io/openhands/agent-server:vasco-test-4675-conversation-read-not-serialized-python-amd64
ghcr.io/openhands/agent-server:9523b2e-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:9523b2e-python-arm64
ghcr.io/openhands/agent-server:9523b2e127e2847b9bb8ba985a9c263229c6b426-python-arm64
ghcr.io/openhands/agent-server:vasco-test-4675-conversation-read-not-serialized-python-arm64
ghcr.io/openhands/agent-server:9523b2e-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:9523b2e-golang
ghcr.io/openhands/agent-server:9523b2e127e2847b9bb8ba985a9c263229c6b426-golang
ghcr.io/openhands/agent-server:vasco-test-4675-conversation-read-not-serialized-golang
ghcr.io/openhands/agent-server:9523b2e-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:9523b2e-java
ghcr.io/openhands/agent-server:9523b2e127e2847b9bb8ba985a9c263229c6b426-java
ghcr.io/openhands/agent-server:vasco-test-4675-conversation-read-not-serialized-java
ghcr.io/openhands/agent-server:9523b2e-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:9523b2e-python
ghcr.io/openhands/agent-server:9523b2e127e2847b9bb8ba985a9c263229c6b426-python
ghcr.io/openhands/agent-server:vasco-test-4675-conversation-read-not-serialized-python
ghcr.io/openhands/agent-server:9523b2e-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 9523b2e-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 9523b2e-python-amd64) are also available if needed

…n unrelated start

#4570 replaced the process-wide `_lifecycle_lock` with per-conversation
lifecycle locks, but the tests it added exercise `_conversation_lifecycle`
directly. Nothing drives the public API, so the guarantee callers actually
depend on — a conversation-scoped read completing while another conversation
is mid-start — is unguarded.

Add that end-to-end regression test: wedge one conversation inside
`_start_event_service` and assert `get_event_service` for a different,
already-live conversation still resolves. The test times out against
611629e (the commit before #4570) and passes on main.
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@VascoSch92
VascoSch92 marked this pull request as ready for review August 27, 2026 13:44

all-hands-bot commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
TOTAL414871730458% 
report-only-changed-files is enabled. No files were changed during this commit :)

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Good taste — Clean, well-targeted regression test that closes the acceptance-criterion gap identified in #4675.

The test drives the public API (start_conversation + get_event_service) rather than poking the lock helper internals directly, so it will catch a regression regardless of how the locking is implemented — exactly what a regression test should do. CI is green on all relevant suites (agent-server-tests, sdk-tests, windows-tests, agent-server-stress-tests).

One very minor note worth considering (not blocking):

await starting in the finally block has no timeout. After release_start.set() the blocked coroutine should unblock quickly, but if start_conversation were to hang for an unrelated reason after the lock release, the test would run forever. Wrapping it in asyncio.wait_for(starting, timeout=5) would bound the worst case. Entirely optional — the current form is readable and consistent with surrounding tests.

No production code changes. No new dependencies. Draft → open when you're happy with it.


[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW
    Pure test addition, zero production code changes. The only risk is a flaky test, and the 5-second asyncio.wait_for guards on both the wedge-entry and the read make that unlikely.

VERDICT:
Worth merging — Core logic is sound, single optional improvement noted.

KEY INSIGHT:
The test correctly targets the public API layer rather than the lock implementation detail, so it remains valid even if the locking strategy changes again.


Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing. See the customization docs for the required frontmatter format.
  2. Re-request a review — the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
  3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.

Resolve with AI? Install the iterate skill in your agent and run /iterate to automatically drive this PR through CI, review, and QA until it's merge-ready.

Was this review helpful? React with 👍 or 👎 to give feedback.

This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

…ion test

Addresses review feedback: after release_start.set() the wedged start should
unblock immediately, but an unrelated hang would otherwise run the test
forever. Bound it like the other two waits in the same test.

@enyst enyst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@VascoSch92
VascoSch92 merged commit b57ba44 into main Aug 27, 2026
39 checks passed
@VascoSch92
VascoSch92 deleted the vasco/test-4675-conversation-read-not-serialized branch August 27, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: process-wide _lifecycle_lock serializes every conversation-scoped request behind start/fork/delete/eviction

3 participants