feat(acp): batch SessionUpdate delivery during replay - #68
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements batching for SessionUpdate delivery during session/load replay to optimize performance. It refactors replay logic into side-effect-free collectors and introduces the _batch_session_updates extension notification with a graceful sequential fallback. Feedback recommends validating the batch size and flush interval parameters during initialization to prevent runtime errors, and updating the proposal document to explicitly mark the rejected default flush interval option as 'Rejected' for historical context.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
ab9d326 to
d65b6ba
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces batching for SessionUpdate notifications during session replay (session/load) to reduce wire roundtrips and improve load times. It refactors the replay mechanism to collect updates in memory first, then chunks and delivers them via a new _batch_session_updates extension notification if supported by the client, with a graceful fallback to sequential delivery. The reviewer noted a potential serialization issue in send_batch_update where calling model_dump() without mode="json" in Pydantic v2 could leave raw Python objects (like datetime) in the payload, potentially causing JSON-RPC serialization failures.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Leoyzen
left a comment
There was a problem hiding this comment.
PR Review & Analysis
Overview
PR quality is solid — OpenSpec artifacts are complete, pure function separation (CPU vs I/O) is clean, backward compatibility is well handled, and test coverage is comprehensive. Below are some suggestions and analysis points for discussion.
1. Code Quality Notes
Done well:
- Pure function refactoring (
_collect_request_updates/_collect_response_updates) makes conversion testable without a client connection — good separation of concerns - Graceful fallback: default
_batch_supported=Falseensures clients without batch support are unaffected - Constructor parameter validation (
batch_size > 0,flush_interval >= 0)
Could improve:
-
notification_flush_intervalis effectively dead code — default is0.0, and the design doc explicitly states no artificial delay is needed (theawaiton eachsend_batch_updateprovides natural flow control). This is YAGNI — a config knob introduced without a real use case. Consider removing it until there's an actual need. -
_tool_call_inputscache state during collection —_collect_response_updatespopulates the cache,_collect_request_updatespops it. If message ordering is incorrect (e.g., aModelRequestwithToolReturnPartappears before the correspondingModelResponsewithToolCallPart), the cache won't match. The design doc mentions this risk but no defensive handling exists. Consider adding a guard or at least a debug log whenpop()misses. -
tests/acp/benchmark_replay.pyis not a pytest test — it's a standalone script withif __name__ == "__main__". Placing it undertests/means pytest's test discovery will import it. Consider moving it toscripts/orbenchmarks/. -
test_from_config_capabilities_not_duplicatedchange — Themonkeypatch.setenv("OPENAI_API_KEY", ...)fix appears unrelated to notification batching. Should this be a separate PR?
2. Why not asyncio.gather for concurrent sends?
Considered alternative: fire all session/update notifications as concurrent tasks, await once via asyncio.gather.
Doesn't work because the underlying transport is a single connection (stdio pipe or TCP socket). All tasks write to the same connection, which has a write lock / internal queue — writes are serialized regardless. asyncio.gather only eliminates Python-level await scheduling overhead (microseconds), not TCP flush cost (milliseconds). Plus, asyncio.gather doesn't guarantee completion order, which breaks ToolCallStart → ToolCallProgress ordering.
The batch approach is fundamentally better: it reduces wire-level message count (600 → 30), reducing JSON serialization, JSON-RPC framing, and client-side dispatch overhead — not just Python scheduling.
3. drain_and_merge reuse consideration
The codebase has a drain_and_merge() component in src/agentpool/orchestrator/event_bus.py that coalesces streaming events (text deltas, tool call deltas, progress) for protocol subscribers. Considered whether this could be reused in replay().
Conclusion: not directly reusable. They operate at different layers:
drain_and_mergecoalesces live streaming events (EventEnvelope/RichAgentStreamEvent) during active runsreplay()converts historicalModelMessages toSessionUpdateobjects for one-shot session loading
Different data types, different lifecycle (continuous vs one-shot). The PR's design doc explicitly considered and rejected a streaming pipe approach (Decision 2), choosing collect-then-send for simplicity.
However, if future work extends batching to live streaming notifications (currently a Non-Goal), the drain_and_merge pattern — particularly _merge_key() + _merge_envelopes() — would be worth referencing for coalescing before batch delivery.
4. Spec context: when is replay needed?
Per ACP spec (checked agent-client-protocol schema):
v1:
session/load: "Stream the entire conversation history back to the client via notifications" —LoadSessionResponseonly carries session metadata (modes,configOptions), no message history in response payloadsession/resume: Resume without replaying history
v2:
- Merged into
session/resumewith optionalreplayFromparameter:replayFrom: null→ resume without replay (v1'ssession/resume)replayFrom: { "type": "start" }→ replay full history (v1'ssession/load)
So replay via notifications is required by spec — the response payload doesn't carry conversation history. The batching optimization in this PR is the right approach: it reduces notification count while staying spec-compliant.
A more fundamental fix (returning history in the response payload) would require a protocol spec change, which is out of scope.
Summary
PR is approvable with minor cleanup suggestions. The batching approach is spec-compliant and architecturally sound. Main actionable items:
- Consider removing
notification_flush_interval(YAGNI) - Move benchmark script out of
tests/ - Split unrelated CI fix into separate PR
|
@Million-mo ci 失败了 |
Refactor ACPNotifications.replay() from sequential per-update session/update notifications to collect-then-batch-send pattern: - Add _collect_request_updates/_collect_response_updates as pure conversion methods returning list[SessionUpdate] (no I/O) - Add send_batch_update() that uses ext_notification _batch_session_updates when client supports it, falls back to sequential session/update otherwise - Add notification_batch_size (default 20) and notification_flush_interval (default 0.0) constructor params to ACPNotifications - Add set_batch_support() for capability-based opt-in - Wire batch support detection into ACPSession.__post_init__ via client_capabilities.field_meta - Keep _replay_request/_replay_response as thin wrappers for backward compatibility - Add 6 new tests covering batch mode, fallback, ordering, custom size, pure collector, and empty messages - Add benchmark script comparing batch vs sequential OpenSpec change: acp-notification-batching Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Remove parameter (YAGNI — no real use case) - Move benchmark script from tests/ to scripts/ to avoid pytest collection - Revert unrelated CI fix from test_from_config_capabilities_not_duplicated - Add defensive debug log when _tool_call_inputs cache misses on ToolReturnPart
…_config_capabilities_not_duplicated CI environment has no OPENAI_API_KEY set; the setenv is required for the test that creates a NativeAgentConfig with model=openai:gpt-4o-mini.
1ec186a to
45d1b4b
Compare
Summary
Optimize ACP
session/loadreplay by batchingSessionUpdatedelivery instead of sending each update as a separatesession/updateJSON-RPC notification.Problem
replay()sends 300-600 individualsession/updatenotifications for a 100-message session, each with a sequentialawaiton TCP flush. Estimated load time: 150-300 seconds.Solution
Collect all
SessionUpdateobjects first (pure CPU conversion, no I/O), then deliver in batches of 20 via_batch_session_updatesext_notification. Clients without batch support automatically fall back to sequential delivery.Changes
src/acp/agent/notifications.py:_replay_request/_replay_responseinto pure_collect_request_updates/_collect_response_updates(returnlist[SessionUpdate], no I/O)send_batch_update()— usesext_notification("_batch_session_updates", ...)when supported, falls back to sequentialsession/updatenotification_batch_size(default 20) andnotification_flush_interval(default 0.0) to__init__set_batch_support(bool)for capability-based opt-inreplay()as collect-then-batch-send_replay_request/_replay_responseas thin wrappers for backward compatibilitysrc/agentpool_server/acp_server/session.py:client_capabilities.field_meta["_batch_session_updates"]inACPSession.__post_init__tests/acp/test_notifications_replay.py: 6 new tests (batch mode, fallback, ordering, custom size, pure collector, empty)tests/acp/benchmark_replay.py: Benchmark script comparing batch vs sequentialDesign
ext_notificationextension mechanism — no protocol spec changesClientCapabilities.field_meta["_batch_session_updates"]_batch_supported=False— sequential fallback when client doesn't opt inOpenSpec
Change:
openspec/changes/acp-notification-batching/(proposal + design + specs + tasks, all complete)Test Results
Note
This PR is based on
develop/agenticand is independent of PR #65 (eliminate-pool-level-agents). The only overlap issession.py(3 lines added in__post_init__), which should rebase cleanly.Co-authored-by: Sisyphus clio-agent@sisyphuslabs.ai