@tickernelz/paperclip-pro-server 2026.928.4 → 2026.928.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +1 -1
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +1 -0
- package/dist/routes/issues.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/acp.d.ts +4 -1
- package/node_modules/@agentclientprotocol/sdk/dist/acp.js +3 -2
- package/node_modules/@agentclientprotocol/sdk/dist/acp.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/connection.d.ts +199 -11
- package/node_modules/@agentclientprotocol/sdk/dist/connection.js +454 -46
- package/node_modules/@agentclientprotocol/sdk/dist/connection.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/connection.test.js +170 -11
- package/node_modules/@agentclientprotocol/sdk/dist/connection.test.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/examples/http-server.js +6 -1
- package/node_modules/@agentclientprotocol/sdk/dist/examples/http-server.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/http-stream.d.ts +2 -0
- package/node_modules/@agentclientprotocol/sdk/dist/http-stream.js +46 -8
- package/node_modules/@agentclientprotocol/sdk/dist/http-stream.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.d.ts +7 -5
- package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.js +36 -29
- package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.js +33 -2
- package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.test.js +87 -0
- package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.test.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.d.ts +6 -1
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.js +78 -30
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.test.js +59 -6
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.test.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/server-websocket-upgrade.test.js +320 -2
- package/node_modules/@agentclientprotocol/sdk/dist/server-websocket-upgrade.test.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/server.d.ts +4 -3
- package/node_modules/@agentclientprotocol/sdk/dist/server.js +49 -21
- package/node_modules/@agentclientprotocol/sdk/dist/server.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/server.test.js +530 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server.test.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/sse.d.ts +3 -1
- package/node_modules/@agentclientprotocol/sdk/dist/sse.js +59 -36
- package/node_modules/@agentclientprotocol/sdk/dist/sse.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/stream-limits.d.ts +15 -0
- package/node_modules/@agentclientprotocol/sdk/dist/stream-limits.js +58 -0
- package/node_modules/@agentclientprotocol/sdk/dist/stream-limits.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/stream.d.ts +4 -1
- package/node_modules/@agentclientprotocol/sdk/dist/stream.js +6 -2
- package/node_modules/@agentclientprotocol/sdk/dist/stream.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/until.d.ts +5 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/until.js +14 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/until.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.d.ts +4 -1
- package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.js +3 -2
- package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/ws-server.js +143 -15
- package/node_modules/@agentclientprotocol/sdk/dist/ws-server.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.d.ts +6 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.js.map +1 -1
- package/node_modules/@agentclientprotocol/sdk/package.json +1 -1
- package/package.json +21 -21
- package/ui-dist/assets/{Agents.production-Cj5rZo69.js → Agents.production-Ou10IBgG.js} +2 -2
- package/ui-dist/assets/{CompanyActivity.production-BsesMNQp.js → CompanyActivity.production-dbz2o26S.js} +1 -1
- package/ui-dist/assets/{CompanyExport-CWmvCY4A.js → CompanyExport-VHEmaNC_.js} +1 -1
- package/ui-dist/assets/{CompanySkills.production-BGtfN-3A.js → CompanySkills.production-DuSUb8DQ.js} +1 -1
- package/ui-dist/assets/{Costs.production-84DycSrt.js → Costs.production-BRz8Rn3s.js} +1 -1
- package/ui-dist/assets/{OrgChart.production-D_VwOg1Z.js → OrgChart.production-Dgq5pMMa.js} +1 -1
- package/ui-dist/assets/{RoutineDetail.production-DJe0NpxJ.js → RoutineDetail.production-B2lkeY8y.js} +1 -1
- package/ui-dist/assets/{Routines.production-VhYuO012.js → Routines.production-CGNSsZDr.js} +1 -1
- package/ui-dist/assets/{definition-DurZnIaP.js → definition-D6jTm4Mv.js} +1 -1
- package/ui-dist/assets/{index-BqdSRbLj.js → index-DGStZYMS.js} +4 -4
- package/ui-dist/index.html +1 -1
- package/ui-dist/sw.js +1 -1
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { serializeSseJson, serializeSseKeepAlive } from "./sse.js";
|
|
2
|
+
/**
|
|
3
|
+
* Streams the JSON text of each message a lease yields as an SSE event. The
|
|
4
|
+
* body takes a message only when its reader asks for one, so messages its
|
|
5
|
+
* client has not read stay queued, and counted, in their connection.
|
|
6
|
+
*/
|
|
2
7
|
export function createSseBody(lease) {
|
|
3
|
-
return new ReadableStream(createSseBodySource(lease)
|
|
8
|
+
return new ReadableStream(createSseBodySource(lease), {
|
|
9
|
+
highWaterMark: 0,
|
|
10
|
+
});
|
|
4
11
|
}
|
|
5
12
|
/** @internal */
|
|
6
13
|
export function createSseBodySource(lease) {
|
|
7
14
|
const encoder = new TextEncoder();
|
|
8
15
|
let keepAliveTimer;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the reader has asked for a chunk it has not been given. With no
|
|
18
|
+
* high-water mark, a reader that is not waiting may still be writing the
|
|
19
|
+
* last chunk out to a client that stopped reading.
|
|
20
|
+
*/
|
|
21
|
+
let isReaderWaiting = false;
|
|
9
22
|
let isReceiving = false;
|
|
10
23
|
let isClosed = false;
|
|
11
24
|
const clearKeepAlive = () => {
|
|
@@ -15,6 +28,9 @@ export function createSseBodySource(lease) {
|
|
|
15
28
|
}
|
|
16
29
|
};
|
|
17
30
|
const enqueueText = (controller, text) => {
|
|
31
|
+
// The reader takes this chunk at once; pull() runs again when it asks for
|
|
32
|
+
// another.
|
|
33
|
+
isReaderWaiting = false;
|
|
18
34
|
try {
|
|
19
35
|
controller.enqueue(encoder.encode(text));
|
|
20
36
|
return true;
|
|
@@ -23,7 +39,6 @@ export function createSseBodySource(lease) {
|
|
|
23
39
|
return false;
|
|
24
40
|
}
|
|
25
41
|
};
|
|
26
|
-
const hasDemand = (controller) => controller.desiredSize !== null && controller.desiredSize > 0;
|
|
27
42
|
const closeBody = (controller) => {
|
|
28
43
|
if (isClosed) {
|
|
29
44
|
return;
|
|
@@ -38,23 +53,34 @@ export function createSseBodySource(lease) {
|
|
|
38
53
|
// Stream may already be cancelled by the consumer.
|
|
39
54
|
}
|
|
40
55
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const errorBody = (controller, error) => {
|
|
57
|
+
if (isClosed) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
isClosed = true;
|
|
61
|
+
clearKeepAlive();
|
|
62
|
+
lease.release();
|
|
63
|
+
controller.error(error);
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Once the stream stops, the body can only end. A waiting reader has taken
|
|
67
|
+
* every chunk, so the pending receive() ends the body, cleanly or with the
|
|
68
|
+
* error. Any other reader may be stuck on a chunk its client will never
|
|
69
|
+
* take, so abort the body rather than leave its response open.
|
|
70
|
+
*/
|
|
71
|
+
const endIfStopped = (controller) => {
|
|
72
|
+
if (lease.stopped.aborted && !isReaderWaiting) {
|
|
73
|
+
errorBody(controller, lease.stopped.reason);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
/** Hands the reader messages for as long as it waits for them. */
|
|
77
|
+
const deliver = async (controller) => {
|
|
78
|
+
if (isReceiving) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
isReceiving = true;
|
|
82
|
+
try {
|
|
83
|
+
while (isReaderWaiting && !isClosed) {
|
|
58
84
|
const result = await lease.receive();
|
|
59
85
|
if (isClosed) {
|
|
60
86
|
return;
|
|
@@ -63,20 +89,42 @@ export function createSseBodySource(lease) {
|
|
|
63
89
|
closeBody(controller);
|
|
64
90
|
return;
|
|
65
91
|
}
|
|
66
|
-
if (!enqueueText(controller,
|
|
92
|
+
if (!enqueueText(controller, serializeSseJson(result.value))) {
|
|
67
93
|
closeBody(controller);
|
|
94
|
+
return;
|
|
68
95
|
}
|
|
96
|
+
// The stream may have stopped while this message was on its way.
|
|
97
|
+
endIfStopped(controller);
|
|
69
98
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
errorBody(controller, error);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
isReceiving = false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
start(controller) {
|
|
109
|
+
lease.stopped.addEventListener("abort", () => {
|
|
110
|
+
endIfStopped(controller);
|
|
111
|
+
}, { once: true });
|
|
112
|
+
keepAliveTimer = setInterval(() => {
|
|
113
|
+
// Only a waiting reader needs a keep-alive.
|
|
114
|
+
if (isClosed || !isReaderWaiting) {
|
|
115
|
+
return;
|
|
75
116
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
117
|
+
if (!enqueueText(controller, serializeSseKeepAlive())) {
|
|
118
|
+
closeBody(controller);
|
|
119
|
+
}
|
|
120
|
+
}, 15_000);
|
|
121
|
+
},
|
|
122
|
+
// With no high-water mark, this runs only when the reader asks for a
|
|
123
|
+
// chunk. It returns at once, so the stream calls it again whenever the
|
|
124
|
+
// reader asks while a receive() is still pending.
|
|
125
|
+
pull(controller) {
|
|
126
|
+
isReaderWaiting = true;
|
|
127
|
+
void deliver(controller);
|
|
80
128
|
},
|
|
81
129
|
cancel() {
|
|
82
130
|
isClosed = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-sse.js","sourceRoot":"","sources":["../src/server-sse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"server-sse.js","sourceRoot":"","sources":["../src/server-sse.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { OutboundMailbox } from "./connection.js";
|
|
3
|
-
import { createSseBodySource } from "./server-sse.js";
|
|
4
|
-
import { serializeSseEvent } from "./sse.js";
|
|
3
|
+
import { createSseBody, createSseBodySource } from "./server-sse.js";
|
|
4
|
+
import { serializeSseEvent, serializeSseKeepAlive } from "./sse.js";
|
|
5
5
|
const message = {
|
|
6
6
|
jsonrpc: "2.0",
|
|
7
7
|
id: 1,
|
|
@@ -35,14 +35,67 @@ describe("createSseBodySource", () => {
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
};
|
|
38
|
-
|
|
38
|
+
await source.pull?.(controller);
|
|
39
39
|
await flushMicrotasks();
|
|
40
40
|
desiredSize = 0;
|
|
41
|
-
mailbox.push(message);
|
|
42
|
-
await
|
|
41
|
+
mailbox.push(JSON.stringify(message));
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
43
43
|
expect(enqueued.map(decodeText)).toEqual([serializeSseEvent(message)]);
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
|
+
describe("createSseBody", () => {
|
|
47
|
+
it("leaves messages queued until its reader asks for one", async () => {
|
|
48
|
+
const mailbox = new OutboundMailbox();
|
|
49
|
+
const lease = mailbox.tryAcquire();
|
|
50
|
+
if (!lease) {
|
|
51
|
+
throw new Error("Expected outbound mailbox lease");
|
|
52
|
+
}
|
|
53
|
+
const reader = createSseBody(lease).getReader();
|
|
54
|
+
mailbox.push(JSON.stringify(message));
|
|
55
|
+
await flushMicrotasks();
|
|
56
|
+
expect(mailbox.hasQueuedMessages).toBe(true);
|
|
57
|
+
const { value } = await reader.read();
|
|
58
|
+
expect(value && decodeText(value)).toBe(serializeSseEvent(message));
|
|
59
|
+
expect(mailbox.hasQueuedMessages).toBe(false);
|
|
60
|
+
await reader.cancel();
|
|
61
|
+
});
|
|
62
|
+
it("ends cleanly when its stream shuts down while the reader waits", async () => {
|
|
63
|
+
const mailbox = new OutboundMailbox();
|
|
64
|
+
const lease = mailbox.tryAcquire();
|
|
65
|
+
if (!lease) {
|
|
66
|
+
throw new Error("Expected outbound mailbox lease");
|
|
67
|
+
}
|
|
68
|
+
const reader = createSseBody(lease).getReader();
|
|
69
|
+
const read = reader.read();
|
|
70
|
+
await flushMicrotasks();
|
|
71
|
+
mailbox.abort();
|
|
72
|
+
await expect(read).resolves.toEqual({ done: true, value: undefined });
|
|
73
|
+
});
|
|
74
|
+
it("sends keep-alives only while its reader waits, and aborts once stopped otherwise", async () => {
|
|
75
|
+
vi.useFakeTimers();
|
|
76
|
+
try {
|
|
77
|
+
const mailbox = new OutboundMailbox();
|
|
78
|
+
const lease = mailbox.tryAcquire();
|
|
79
|
+
if (!lease) {
|
|
80
|
+
throw new Error("Expected outbound mailbox lease");
|
|
81
|
+
}
|
|
82
|
+
const reader = createSseBody(lease).getReader();
|
|
83
|
+
// The reader takes a keep-alive, then stops asking, as if stuck writing
|
|
84
|
+
// it to a client that stopped reading.
|
|
85
|
+
const read = reader.read();
|
|
86
|
+
await vi.advanceTimersByTimeAsync(15_000);
|
|
87
|
+
const { value } = await read;
|
|
88
|
+
expect(value && decodeText(value)).toBe(serializeSseKeepAlive());
|
|
89
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
90
|
+
// Even a clean shutdown aborts the body, and no keep-alives piled up.
|
|
91
|
+
mailbox.abort();
|
|
92
|
+
await expect(reader.read()).rejects.toThrow("ACP connection is closed");
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
vi.useRealTimers();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
46
99
|
async function flushMicrotasks() {
|
|
47
100
|
await Promise.resolve();
|
|
48
101
|
await Promise.resolve();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-sse.test.js","sourceRoot":"","sources":["../src/server-sse.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"server-sse.test.js","sourceRoot":"","sources":["../src/server-sse.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ import { Connection as JsonRpcConnection, batchNotification, batchRequest, } fro
|
|
|
5
5
|
import { HEADER_CONNECTION_ID, JSON_MIME_TYPE } from "./protocol.js";
|
|
6
6
|
import { AcpServer } from "./server.js";
|
|
7
7
|
import { createTestAgentApp, TestAgent } from "./test-support/test-agent.js";
|
|
8
|
+
import { until } from "./test-support/until.js";
|
|
8
9
|
import { handleWebSocketConnection } from "./ws-server.js";
|
|
9
10
|
const initializeRequest = {
|
|
10
11
|
jsonrpc: "2.0",
|
|
@@ -592,6 +593,305 @@ describe("AcpServer prepared WebSocket upgrades", () => {
|
|
|
592
593
|
await registry.closeAll();
|
|
593
594
|
}
|
|
594
595
|
});
|
|
596
|
+
it("closes the socket for session IDs and request IDs longer than maxIdLength", async () => {
|
|
597
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
598
|
+
const registry = new ConnectionRegistry({ maxIdLength: 8 });
|
|
599
|
+
const agent = createTestAgentApp();
|
|
600
|
+
const tooLong = "s".repeat(9);
|
|
601
|
+
const frames = [
|
|
602
|
+
{
|
|
603
|
+
jsonrpc: "2.0",
|
|
604
|
+
method: "session/cancel",
|
|
605
|
+
params: { sessionId: tooLong },
|
|
606
|
+
},
|
|
607
|
+
{ ...sessionNewRequest, id: tooLong },
|
|
608
|
+
];
|
|
609
|
+
try {
|
|
610
|
+
for (const [index, frame] of frames.entries()) {
|
|
611
|
+
const socket = new FakeServerSocket();
|
|
612
|
+
const session = handleWebSocketConnection(socket, { registry, agent });
|
|
613
|
+
socket.receive(JSON.stringify(initializeRequest));
|
|
614
|
+
await readSentMessage(socket);
|
|
615
|
+
socket.receive(JSON.stringify(frame));
|
|
616
|
+
await session.closed;
|
|
617
|
+
expect(socket.closeCode).toBe(1008);
|
|
618
|
+
expect(socket.closeReason).toBe(`${index === 0 ? "Session" : "Request"} ID exceeds maxIdLength (8)`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
warn.mockRestore();
|
|
623
|
+
await registry.closeAll();
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
it("holds back client requests while the socket holds maxBufferedBytes", async () => {
|
|
627
|
+
const registry = new ConnectionRegistry({ maxBufferedBytes: 1024 });
|
|
628
|
+
const agent = createTestAgentApp();
|
|
629
|
+
const socket = new FakeServerSocket();
|
|
630
|
+
handleWebSocketConnection(socket, { registry, agent });
|
|
631
|
+
try {
|
|
632
|
+
socket.receive(JSON.stringify(initializeRequest));
|
|
633
|
+
await readSentMessage(socket);
|
|
634
|
+
// The client has not read what the socket holds, so the request waits.
|
|
635
|
+
socket.bufferedAmount = 1024;
|
|
636
|
+
const reads = socket.bufferedAmountReads;
|
|
637
|
+
socket.receive(JSON.stringify(sessionNewRequest));
|
|
638
|
+
await until(() => socket.bufferedAmountReads > reads);
|
|
639
|
+
await delay(20);
|
|
640
|
+
expect(socket.sent).toEqual([]);
|
|
641
|
+
socket.bufferedAmount = 0;
|
|
642
|
+
await expect(readSentMessage(socket)).resolves.toMatchObject({
|
|
643
|
+
id: sessionNewRequest.id,
|
|
644
|
+
result: { sessionId: expect.any(String) },
|
|
645
|
+
});
|
|
646
|
+
expect(socket.closeCount).toBe(0);
|
|
647
|
+
}
|
|
648
|
+
finally {
|
|
649
|
+
socket.close();
|
|
650
|
+
await registry.closeAll();
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
it("holds back batches the agent answers while the socket holds maxBufferedBytes", async () => {
|
|
654
|
+
const registry = new ConnectionRegistry({ maxBufferedBytes: 4096 });
|
|
655
|
+
const agent = createProtocolAgent(2);
|
|
656
|
+
const socket = new FakeServerSocket();
|
|
657
|
+
handleWebSocketConnection(socket, { registry, agent });
|
|
658
|
+
try {
|
|
659
|
+
socket.receive(JSON.stringify(v2InitializeRequest));
|
|
660
|
+
await readSentMessage(socket);
|
|
661
|
+
// Each of these gets an error reply, and none is a request.
|
|
662
|
+
socket.bufferedAmount = 4096;
|
|
663
|
+
const reads = socket.bufferedAmountReads;
|
|
664
|
+
socket.receive(JSON.stringify([]));
|
|
665
|
+
socket.receive(JSON.stringify([
|
|
666
|
+
{ id: 1, padding: "p" },
|
|
667
|
+
{ jsonrpc: "2.0", method: "_vendor/acme/notification" },
|
|
668
|
+
]));
|
|
669
|
+
await until(() => socket.bufferedAmountReads > reads);
|
|
670
|
+
await delay(20);
|
|
671
|
+
expect(socket.sent).toEqual([]);
|
|
672
|
+
socket.bufferedAmount = 0;
|
|
673
|
+
await expect(readSentWireMessage(socket)).resolves.toMatchObject({
|
|
674
|
+
error: { code: -32600 },
|
|
675
|
+
});
|
|
676
|
+
await expect(readSentWireMessage(socket)).resolves.toMatchObject([
|
|
677
|
+
{ error: { code: -32600 } },
|
|
678
|
+
]);
|
|
679
|
+
}
|
|
680
|
+
finally {
|
|
681
|
+
socket.close();
|
|
682
|
+
await registry.closeAll();
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
it("paces agent output to what the socket has sent", async () => {
|
|
686
|
+
const registry = new ConnectionRegistry({ maxBufferedBytes: 4096 });
|
|
687
|
+
const sent = [];
|
|
688
|
+
const agent = createAgentApp({ name: "burst-agent" })
|
|
689
|
+
.onRequest(methods.agent.initialize, () => ({
|
|
690
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
691
|
+
agentCapabilities: {},
|
|
692
|
+
}))
|
|
693
|
+
.onRequest(methods.agent.session.prompt, async (c) => {
|
|
694
|
+
for (let index = 0; index < 50; index++) {
|
|
695
|
+
await c.client.notify(methods.client.session.update, {
|
|
696
|
+
sessionId: c.params.sessionId,
|
|
697
|
+
update: {
|
|
698
|
+
sessionUpdate: "agent_message_chunk",
|
|
699
|
+
content: { type: "text", text: "x".repeat(1000) },
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
sent.push(index);
|
|
703
|
+
}
|
|
704
|
+
return { stopReason: "end_turn" };
|
|
705
|
+
});
|
|
706
|
+
const socket = new FakeServerSocket();
|
|
707
|
+
handleWebSocketConnection(socket, { registry, agent });
|
|
708
|
+
try {
|
|
709
|
+
socket.receive(JSON.stringify(initializeRequest));
|
|
710
|
+
await readSentMessage(socket);
|
|
711
|
+
// The client stops reading, so everything sent stays in the socket.
|
|
712
|
+
socket.holdsSentData = true;
|
|
713
|
+
socket.receive(JSON.stringify({
|
|
714
|
+
jsonrpc: "2.0",
|
|
715
|
+
id: 5,
|
|
716
|
+
method: "session/prompt",
|
|
717
|
+
params: {
|
|
718
|
+
sessionId: "session-1",
|
|
719
|
+
prompt: [{ type: "text", text: "go" }],
|
|
720
|
+
},
|
|
721
|
+
}));
|
|
722
|
+
await until(() => socket.bufferedAmount >= 4096);
|
|
723
|
+
await delay(20);
|
|
724
|
+
expect(sent.length).toBeGreaterThan(0);
|
|
725
|
+
expect(sent.length).toBeLessThan(10);
|
|
726
|
+
expect(socket.sent.length).toBeLessThan(10);
|
|
727
|
+
// Once the client reads, the agent finishes.
|
|
728
|
+
socket.holdsSentData = false;
|
|
729
|
+
socket.bufferedAmount = 0;
|
|
730
|
+
await until(() => socket.sent.some((data) => JSON.parse(data).id === 5), 2_000);
|
|
731
|
+
expect(sent).toHaveLength(50);
|
|
732
|
+
expect(socket.closeCount).toBe(0);
|
|
733
|
+
}
|
|
734
|
+
finally {
|
|
735
|
+
socket.close();
|
|
736
|
+
await registry.closeAll();
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
it("closes the socket when its client reads none of its output for maxOutputStallMs", async () => {
|
|
740
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
741
|
+
const registry = new ConnectionRegistry({
|
|
742
|
+
maxBufferedBytes: 1024,
|
|
743
|
+
maxOutputStallMs: 50,
|
|
744
|
+
});
|
|
745
|
+
// A reply larger than the socket may hold, after which the agent has
|
|
746
|
+
// nothing more to send.
|
|
747
|
+
const agent = createTestAgentApp({
|
|
748
|
+
newSession: () => ({
|
|
749
|
+
sessionId: "session-1",
|
|
750
|
+
_meta: { padding: "p".repeat(2000) },
|
|
751
|
+
}),
|
|
752
|
+
});
|
|
753
|
+
const socket = new FakeServerSocket();
|
|
754
|
+
const session = handleWebSocketConnection(socket, { registry, agent });
|
|
755
|
+
try {
|
|
756
|
+
socket.receive(JSON.stringify(initializeRequest));
|
|
757
|
+
await readSentMessage(socket);
|
|
758
|
+
socket.holdsSentData = true;
|
|
759
|
+
socket.receive(JSON.stringify(sessionNewRequest));
|
|
760
|
+
await session.closed;
|
|
761
|
+
expect(socket.sent).toHaveLength(1);
|
|
762
|
+
expect(socket.closeCode).toBe(1008);
|
|
763
|
+
expect(socket.closeReason).toBe("Connection output stalled for maxOutputStallMs (50)");
|
|
764
|
+
}
|
|
765
|
+
finally {
|
|
766
|
+
warn.mockRestore();
|
|
767
|
+
await registry.closeAll();
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
it("closes a full socket once its connection shuts down", async () => {
|
|
771
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
772
|
+
const registry = new ConnectionRegistry({
|
|
773
|
+
maxBufferedBytes: 1024,
|
|
774
|
+
maxIdLength: 8,
|
|
775
|
+
});
|
|
776
|
+
let agentWriter;
|
|
777
|
+
const agent = {
|
|
778
|
+
connect(stream) {
|
|
779
|
+
void (async () => {
|
|
780
|
+
const reader = stream.readable.getReader();
|
|
781
|
+
const { value } = await reader.read();
|
|
782
|
+
agentWriter = stream.writable.getWriter();
|
|
783
|
+
await agentWriter.write({
|
|
784
|
+
jsonrpc: "2.0",
|
|
785
|
+
id: value.id,
|
|
786
|
+
result: {
|
|
787
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
788
|
+
agentCapabilities: {},
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
})();
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
const socket = new FakeServerSocket();
|
|
795
|
+
const session = handleWebSocketConnection(socket, { registry, agent });
|
|
796
|
+
try {
|
|
797
|
+
socket.receive(JSON.stringify(initializeRequest));
|
|
798
|
+
await readSentMessage(socket);
|
|
799
|
+
if (!agentWriter) {
|
|
800
|
+
throw new Error("Expected the agent to have answered initialize");
|
|
801
|
+
}
|
|
802
|
+
// The client stops reading, so this fills the socket.
|
|
803
|
+
socket.holdsSentData = true;
|
|
804
|
+
void agentWriter.write({
|
|
805
|
+
jsonrpc: "2.0",
|
|
806
|
+
method: "_vendor/acme/notification",
|
|
807
|
+
params: { padding: "p".repeat(2000) },
|
|
808
|
+
});
|
|
809
|
+
await readSentMessage(socket);
|
|
810
|
+
// A session ID the connection refuses shuts it down, which closes the
|
|
811
|
+
// socket without waiting for the client to read.
|
|
812
|
+
void agentWriter.write({
|
|
813
|
+
jsonrpc: "2.0",
|
|
814
|
+
method: "_vendor/acme/notification",
|
|
815
|
+
params: { sessionId: "s".repeat(9) },
|
|
816
|
+
});
|
|
817
|
+
await session.closed;
|
|
818
|
+
expect(socket.closeCode).toBe(1008);
|
|
819
|
+
expect(socket.closeReason).toBe("Session ID exceeds maxIdLength (8)");
|
|
820
|
+
}
|
|
821
|
+
finally {
|
|
822
|
+
warn.mockRestore();
|
|
823
|
+
await registry.closeAll();
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
it("closes the socket when the client sends more than maxBufferedBytes while earlier messages wait", async () => {
|
|
827
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
828
|
+
const initialize = createDeferred();
|
|
829
|
+
const registry = new ConnectionRegistry({ maxBufferedBytes: 4096 });
|
|
830
|
+
const agent = createTestAgentApp({ initialize: () => initialize.promise });
|
|
831
|
+
const connection = registry.createPendingConnection(agent);
|
|
832
|
+
const socket = new FakeServerSocket();
|
|
833
|
+
const session = handleWebSocketConnection(socket, {
|
|
834
|
+
registry,
|
|
835
|
+
agent,
|
|
836
|
+
connection,
|
|
837
|
+
});
|
|
838
|
+
const padding = JSON.stringify({
|
|
839
|
+
jsonrpc: "2.0",
|
|
840
|
+
method: "_vendor/acme/padding",
|
|
841
|
+
params: { padding: "p".repeat(1500) },
|
|
842
|
+
});
|
|
843
|
+
try {
|
|
844
|
+
// Frames wait behind the initialize request until the agent answers.
|
|
845
|
+
socket.receive(JSON.stringify(initializeRequest));
|
|
846
|
+
await delay(0);
|
|
847
|
+
socket.receive(padding);
|
|
848
|
+
expect(socket.closeCount).toBe(0);
|
|
849
|
+
socket.receive(padding);
|
|
850
|
+
await session.closed;
|
|
851
|
+
expect(socket.closeCode).toBe(1008);
|
|
852
|
+
expect(socket.closeReason).toBe("Connection exceeds maxBufferedBytes (4096)");
|
|
853
|
+
expect(warn).toHaveBeenCalledWith(`Closing ACP connection ${connection.connectionId}:`, "Connection exceeds maxBufferedBytes (4096)");
|
|
854
|
+
}
|
|
855
|
+
finally {
|
|
856
|
+
initialize.resolve({
|
|
857
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
858
|
+
agentCapabilities: {},
|
|
859
|
+
});
|
|
860
|
+
warn.mockRestore();
|
|
861
|
+
await registry.closeAll();
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
it("counts empty frames waiting to be handled toward maxBufferedBytes", async () => {
|
|
865
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
866
|
+
const initialize = createDeferred();
|
|
867
|
+
const registry = new ConnectionRegistry({ maxBufferedBytes: 4096 });
|
|
868
|
+
const agent = createTestAgentApp({ initialize: () => initialize.promise });
|
|
869
|
+
const socket = new FakeServerSocket();
|
|
870
|
+
const session = handleWebSocketConnection(socket, { registry, agent });
|
|
871
|
+
try {
|
|
872
|
+
// Frames wait behind the initialize request until the agent answers.
|
|
873
|
+
// Each also holds more than its text, which counts 1 KiB, so four fit.
|
|
874
|
+
socket.receive(JSON.stringify(initializeRequest));
|
|
875
|
+
await delay(0);
|
|
876
|
+
let frames = 0;
|
|
877
|
+
while (socket.closeCount === 0 && frames < 100) {
|
|
878
|
+
socket.receive("");
|
|
879
|
+
frames += 1;
|
|
880
|
+
}
|
|
881
|
+
expect(frames).toBe(5);
|
|
882
|
+
await session.closed;
|
|
883
|
+
expect(socket.closeCode).toBe(1008);
|
|
884
|
+
expect(socket.closeReason).toBe("Connection exceeds maxBufferedBytes (4096)");
|
|
885
|
+
}
|
|
886
|
+
finally {
|
|
887
|
+
initialize.resolve({
|
|
888
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
889
|
+
agentCapabilities: {},
|
|
890
|
+
});
|
|
891
|
+
warn.mockRestore();
|
|
892
|
+
await registry.closeAll();
|
|
893
|
+
}
|
|
894
|
+
});
|
|
595
895
|
it("forwards post-initialize batches and sends one response-array frame", async () => {
|
|
596
896
|
const registry = new ConnectionRegistry();
|
|
597
897
|
const agent = createProtocolAgent(2);
|
|
@@ -674,7 +974,7 @@ describe("AcpServer prepared WebSocket upgrades", () => {
|
|
|
674
974
|
]));
|
|
675
975
|
await loadStarted.promise;
|
|
676
976
|
expect(connection.pendingRoutes.get("number:30")).toBe("connection");
|
|
677
|
-
expect(connection.sessionStreams.has(sessionId)).toBe(
|
|
977
|
+
expect(connection.sessionStreams.has(sessionId)).toBe(false);
|
|
678
978
|
finishLoad.resolve();
|
|
679
979
|
const response = await readSentWireMessage(socket);
|
|
680
980
|
expect(response).toMatchObject([{ jsonrpc: "2.0", id: 30 }]);
|
|
@@ -777,7 +1077,7 @@ describe("AcpServer prepared WebSocket upgrades", () => {
|
|
|
777
1077
|
[`number:${firstRequest.id}`, { session: sessionId }],
|
|
778
1078
|
[`number:${secondRequest.id}`, "connection"],
|
|
779
1079
|
]));
|
|
780
|
-
expect(connection.sessionStreams.has(sessionId)).toBe(
|
|
1080
|
+
expect(connection.sessionStreams.has(sessionId)).toBe(false);
|
|
781
1081
|
socket.receive(JSON.stringify([
|
|
782
1082
|
{
|
|
783
1083
|
jsonrpc: "2.0",
|
|
@@ -935,6 +1235,9 @@ function createDeferred() {
|
|
|
935
1235
|
});
|
|
936
1236
|
return { promise, resolve, reject };
|
|
937
1237
|
}
|
|
1238
|
+
function delay(ms) {
|
|
1239
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1240
|
+
}
|
|
938
1241
|
function readSentMessage(socket) {
|
|
939
1242
|
return readSentWireMessage(socket).then((message) => {
|
|
940
1243
|
if (Array.isArray(message)) {
|
|
@@ -959,11 +1262,26 @@ class FakeServerSocket {
|
|
|
959
1262
|
sent = [];
|
|
960
1263
|
listeners = new Map();
|
|
961
1264
|
onSend;
|
|
1265
|
+
/** Whether sent data stays in `bufferedAmount`, as if the peer stopped reading. */
|
|
1266
|
+
holdsSentData = false;
|
|
1267
|
+
/** How many times `bufferedAmount` was read, such as by a session waiting on it. */
|
|
1268
|
+
bufferedAmountReads = 0;
|
|
962
1269
|
closeCount = 0;
|
|
963
1270
|
closeCode;
|
|
964
1271
|
closeReason;
|
|
1272
|
+
unsentBytes = 0;
|
|
1273
|
+
get bufferedAmount() {
|
|
1274
|
+
this.bufferedAmountReads += 1;
|
|
1275
|
+
return this.unsentBytes;
|
|
1276
|
+
}
|
|
1277
|
+
set bufferedAmount(value) {
|
|
1278
|
+
this.unsentBytes = value;
|
|
1279
|
+
}
|
|
965
1280
|
send(data) {
|
|
966
1281
|
this.sent.push(data);
|
|
1282
|
+
if (this.holdsSentData) {
|
|
1283
|
+
this.unsentBytes += data.length;
|
|
1284
|
+
}
|
|
967
1285
|
this.onSend?.(data);
|
|
968
1286
|
this.onSend = undefined;
|
|
969
1287
|
}
|