@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
88## [ Unreleased]
99
10+ Remote agents: the Streamable HTTP and WebSocket transport from the ACP RFD, on plain ` http:// ` and
11+ ` https:// ` , plus the fixes found reviewing it. Two behaviour changes need attention when upgrading:
12+ building a second client on an already-connected transport now fails at construction, and
13+ ` JacksonAcpJsonMapper ` built from a bare ` new ObjectMapper() ` is now strict about unknown fields.
14+
1015### Added
1116
1217- ** Streamable HTTP transport** (RFD
@@ -16,20 +21,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1621 / ` Acp-Session-Id ` headers, ` DELETE ` to close); the new module ** ` acp-streamable-http-jetty ` ** provides
1722 ` StreamableHttpAcpAgentTransport ` , a Jetty listener that serves HTTP/SSE and a WebSocket upgrade on the
1823 same path and hosts one agent per remote connection through ` AcpAgentFactory ` and ` RemoteAcpConnection ` .
19- Header names and path match the TypeScript and Rust SDKs.
24+ Header names and path match the TypeScript and Rust SDKs. Each SSE stream is a mailbox: events sent
25+ while no subscriber is attached, or not yet written to one that went away, are delivered in order
26+ when the client reopens it. Closing a connection (` DELETE ` ) cancels its in-flight prompts.
27+ - ** Limits and keep-alive for the HTTP agent transport** (` StreamableHttpAcpAgentTransportOptions ` ): POST bodies
28+ are capped (16 MiB by default, 413 beyond), the WebSocket send queue and the number of provisional
29+ ` session/load ` streams per connection are bounded, a failed ` session/load ` leaves no provisional state,
30+ the SSE mailbox and per-subscriber queue limits are configurable, attached streams get a ` : keep-alive `
31+ comment every 15 s so proxies do not cut idle connections, and a new GET on a stream takes it over
32+ from a subscriber the server may not yet know is dead instead of fanning out duplicates.
33+ - ** HTTP/2 over plain ` http:// ` .** The RFD requires HTTP/2, and localhost without TLS is a first-class
34+ deployment. Over cleartext the JDK client only offers the h2c upgrade on a request without a body, so
35+ ` initialize ` , a POST, went out on HTTP/1.1. ` StreamableHttpAcpClientTransport ` now sends a bodiless
36+ OPTIONS first on ` http:// ` endpoints, and every request, streams included, runs on HTTP/2.
37+ - ** Client sessions learn that their transport died.** ` AcpClientTransport.awaitTermination() ` (default:
38+ never) is implemented by the Streamable HTTP and WebSocket client transports; ` AcpClientSession ` fails
39+ pending requests at once with the cause, and every later request, instead of waiting out the request
40+ timeout.
2041- ` CancelNotification ` carries ` _meta ` .
42+ - ` AcpSyncClient(AcpAsyncClient) ` is public: the supported way to have both APIs over one session.
2143
2244### Changed
2345
2446- ** Single-turn enforcement is per logical session.** ` AcpAgentSession ` keyed its active-prompt lock
2547 per transport connection; over HTTP one connection carries many sessions, so it is now keyed by
2648 ` sessionId ` (` hasActivePrompt(sessionId) ` , ` getActivePromptSessionIds() ` ), matching the Kotlin SDK.
2749 Stdio and WebSocket agents, which see one session per connection, behave as before. (#7 , #9 )
50+ - ** Unknown-field policy moved from the schema to the mapper (#10 ).** Every schema record carried
51+ ` @JsonIgnoreProperties(ignoreUnknown = true) ` , which made the SDK tolerate fields it does not know
52+ (deliberate: the spec adds fields between releases and a newer agent must keep working) but also
53+ defeated any consumer's strict ` ObjectMapper ` , since a class-level annotation wins over
54+ ` FAIL_ON_UNKNOWN_PROPERTIES ` . The annotations are gone. The default mapper,
55+ ` JacksonAcpJsonMapper.defaultObjectMapper() ` , is lenient and logs each ignored property at DEBUG
56+ so spec drift is observable; a consumer who passes a strict mapper to ` JacksonAcpJsonMapper ` now
57+ gets strict behaviour. ** If you construct ` JacksonAcpJsonMapper ` with a bare ` new ObjectMapper() ` ,
58+ you now get Jackson's default, which fails on unknown fields** : start from
59+ ` defaultObjectMapper() ` instead. Unknown fields are not routed into ` _meta ` , which has its own
60+ spec-defined meaning. Reported by @KallivdH .
61+ - ** One shared timeout scheduler.** Every ` AcpClientSession ` and ` AcpAgentSession ` created its own
62+ scheduled thread pool for request timeouts; over the Streamable HTTP transport, which hosts one agent
63+ session per remote connection, that was one idle thread per connection. Timeouts now run on a single
64+ library-owned daemon timer (` AcpSchedulers.timeouts() ` ).
65+ - The scheduler-hygiene test (` SchedulerBestPracticesTest ` ) now scans every module's production sources,
66+ not only ` acp-core ` .
2867
2968### Fixed
3069
31- - ` WebSocketAcpAgentTransport ` read its client session field twice around a null check while the
32- socket's ` onClose ` could clear it, an occasional ` NullPointerException ` on close.
3370- ** A second client on an already-connected transport now fails at construction.** A transport
3471 instance carries exactly one session. ` StdioAcpClientTransport.connect() ` had no once-only guard
3572 (the WebSocket and agent transports did), so a second ` AcpClient.async(transport) ` or
@@ -42,7 +79,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4279 stdio transport refuses a second ` connect() ` , and ` AcpClientSession ` / ` AcpAgentSession ` surface a
4380 connect or start failure instead of dropping it: construction throws ` IllegalStateException ` when
4481 the transport refuses synchronously, and every later request fails immediately with the cause.
45-
4682- ** Second prompt on a session could be rejected or hang under CPU contention (#14 ).** The agent
4783 released its single-turn prompt lock in ` doFinally ` , * after* the response had already reached the
4884 client. On a starved machine (` taskset -c 0 ` , busy CI runners) the client's next prompt arrived
@@ -57,47 +93,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5793 already was, and a failed emission in the in-memory pair no longer terminates the agent. The
5894 reporter's repro is now a test, and CI runs the contention tests pinned to one CPU. Reported by
5995 @krickert .
60- - ** Streamable HTTP agent transport hardening** (` StreamableHttpAcpAgentTransportOptions ` ): POST bodies
61- are capped (16 MiB by default, 413 beyond), the WebSocket send queue and the number of provisional
62- ` session/load ` streams per connection are bounded, a failed ` session/load ` leaves no provisional state,
63- the SSE mailbox and per-subscriber queue limits are configurable, attached streams get a ` : keep-alive `
64- comment every 15 s so proxies do not cut idle connections, and a new GET on a stream takes it over
65- from a subscriber the server may not yet know is dead instead of fanning out duplicates.
66- - ** HTTP/2 over plain ` http:// ` .** The RFD requires HTTP/2, and localhost without TLS is a first-class
67- deployment. Over cleartext the JDK client only offers the h2c upgrade on a request without a body, so
68- ` initialize ` , a POST, went out on HTTP/1.1. ` StreamableHttpAcpClientTransport ` now sends a bodiless
69- OPTIONS first on ` http:// ` endpoints, and every request, streams included, runs on HTTP/2.
70- - ** Client sessions learn that their transport died.** ` AcpClientTransport.awaitTermination() ` (default:
71- never) is implemented by the Streamable HTTP and WebSocket client transports; ` AcpClientSession ` fails
72- pending requests at once with the cause, and every later request, instead of waiting out the request
73- timeout.
74- - ** One shared timeout scheduler.** Every ` AcpClientSession ` and ` AcpAgentSession ` created its own
75- scheduled thread pool for request timeouts; over the Streamable HTTP transport, which hosts one agent
76- session per remote connection, that was one idle thread per connection. Timeouts now run on a single
77- library-owned daemon timer (` AcpSchedulers.timeouts() ` ).
78- - The scheduler-hygiene test (` SchedulerBestPracticesTest ` ) now scans every module's production sources,
79- not only ` acp-core ` .
96+ - ` WebSocketAcpAgentTransport ` read its client session field twice around a null check while the
97+ socket's ` onClose ` could clear it, an occasional ` NullPointerException ` on close.
8098- The agent session now reads ` sessionId ` from typed ` PromptRequest ` /` CancelNotification ` params as
8199 well as from maps, so in-process transports get the right lock owner in logs and cancel matching.
82100
83- ### Changed
84-
85- - ** Unknown-field policy moved from the schema to the mapper (#10 ).** Every schema record carried
86- ` @JsonIgnoreProperties(ignoreUnknown = true) ` , which made the SDK tolerate fields it does not know
87- (deliberate: the spec adds fields between releases and a newer agent must keep working) but also
88- defeated any consumer's strict ` ObjectMapper ` , since a class-level annotation wins over
89- ` FAIL_ON_UNKNOWN_PROPERTIES ` . The annotations are gone. The default mapper,
90- ` JacksonAcpJsonMapper.defaultObjectMapper() ` , is lenient and logs each ignored property at DEBUG
91- so spec drift is observable; a consumer who passes a strict mapper to ` JacksonAcpJsonMapper ` now
92- gets strict behaviour. ** If you construct ` JacksonAcpJsonMapper ` with a bare ` new ObjectMapper() ` ,
93- you now get Jackson's default, which fails on unknown fields** : start from
94- ` defaultObjectMapper() ` instead. Unknown fields are not routed into ` _meta ` , which has its own
95- spec-defined meaning. Reported by @KallivdH .
96-
97- ### Added
98-
99- - ` AcpSyncClient(AcpAsyncClient) ` is public: the supported way to have both APIs over one session.
100-
101101## [ 0.17.0] - 2026-08-28
102102
103103Wire-format correction. No public API change: every constructor and accessor is unchanged, and the
0 commit comments