Skip to content

Commit 289162d

Browse files
author
Mark Pollack
committed
Prepare the changelog for 0.18.0; bound WebSocket payload logging
The Unreleased section had grown two Changed and two Added headings over the day's commits; it is now one Added, Changed and Fixed, with a short summary naming the two behaviour changes that need attention on upgrade. The Streamable HTTP WebSocket connection logged whole message payloads at DEBUG. Full-payload diagnostics stalled two 0.16.0 release runs on the 500 KB WebSocket fixture; the rest of the SDK logs lengths only, and so does this now.
1 parent 99c7e0f commit 289162d

2 files changed

Lines changed: 44 additions & 44 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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

103103
Wire-format correction. No public API change: every constructor and accessor is unchanged, and the

‎acp-streamable-http-jetty/src/main/java/com/agentclientprotocol/sdk/agent/transport/StreamableHttpWebSocketConnection.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ else if (message instanceof AcpSchema.JSONRPCRequest request
106106
void sendToClient(JSONRPCMessage message) {
107107
try {
108108
String payload = jsonMapper.writeValueAsString(message);
109-
logger.debug("Sending streamable ACP WebSocket message: {}", payload);
109+
logger.debug("Sending streamable ACP WebSocket message ({} characters)", payload.length());
110110
outboundSender.send(payload);
111111
}
112112
catch (Exception e) {
@@ -258,7 +258,7 @@ public void onOpen(Session session) {
258258

259259
@OnWebSocketMessage
260260
public void onMessage(Session session, String message) {
261-
logger.debug("Received streamable ACP WebSocket message: {}", message);
261+
logger.debug("Received streamable ACP WebSocket message ({} characters)", message.length());
262262

263263
try {
264264
JSONRPCMessage jsonRpcMessage = AcpSchema.deserializeJsonRpcMessage(jsonMapper, message);

0 commit comments

Comments
 (0)