@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.
Files changed (69) hide show
  1. package/dist/build-info.json +1 -1
  2. package/dist/routes/issues.d.ts.map +1 -1
  3. package/dist/routes/issues.js +1 -0
  4. package/dist/routes/issues.js.map +1 -1
  5. package/node_modules/@agentclientprotocol/sdk/dist/acp.d.ts +4 -1
  6. package/node_modules/@agentclientprotocol/sdk/dist/acp.js +3 -2
  7. package/node_modules/@agentclientprotocol/sdk/dist/acp.js.map +1 -1
  8. package/node_modules/@agentclientprotocol/sdk/dist/connection.d.ts +199 -11
  9. package/node_modules/@agentclientprotocol/sdk/dist/connection.js +454 -46
  10. package/node_modules/@agentclientprotocol/sdk/dist/connection.js.map +1 -1
  11. package/node_modules/@agentclientprotocol/sdk/dist/connection.test.js +170 -11
  12. package/node_modules/@agentclientprotocol/sdk/dist/connection.test.js.map +1 -1
  13. package/node_modules/@agentclientprotocol/sdk/dist/examples/http-server.js +6 -1
  14. package/node_modules/@agentclientprotocol/sdk/dist/examples/http-server.js.map +1 -1
  15. package/node_modules/@agentclientprotocol/sdk/dist/http-stream.d.ts +2 -0
  16. package/node_modules/@agentclientprotocol/sdk/dist/http-stream.js +46 -8
  17. package/node_modules/@agentclientprotocol/sdk/dist/http-stream.js.map +1 -1
  18. package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.d.ts +7 -5
  19. package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.js +36 -29
  20. package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.js.map +1 -1
  21. package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.js +33 -2
  22. package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.js.map +1 -1
  23. package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.test.js +87 -0
  24. package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.test.js.map +1 -1
  25. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.d.ts +6 -1
  26. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.js +78 -30
  27. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.js.map +1 -1
  28. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.test.js +59 -6
  29. package/node_modules/@agentclientprotocol/sdk/dist/server-sse.test.js.map +1 -1
  30. package/node_modules/@agentclientprotocol/sdk/dist/server-websocket-upgrade.test.js +320 -2
  31. package/node_modules/@agentclientprotocol/sdk/dist/server-websocket-upgrade.test.js.map +1 -1
  32. package/node_modules/@agentclientprotocol/sdk/dist/server.d.ts +4 -3
  33. package/node_modules/@agentclientprotocol/sdk/dist/server.js +49 -21
  34. package/node_modules/@agentclientprotocol/sdk/dist/server.js.map +1 -1
  35. package/node_modules/@agentclientprotocol/sdk/dist/server.test.js +530 -0
  36. package/node_modules/@agentclientprotocol/sdk/dist/server.test.js.map +1 -1
  37. package/node_modules/@agentclientprotocol/sdk/dist/sse.d.ts +3 -1
  38. package/node_modules/@agentclientprotocol/sdk/dist/sse.js +59 -36
  39. package/node_modules/@agentclientprotocol/sdk/dist/sse.js.map +1 -1
  40. package/node_modules/@agentclientprotocol/sdk/dist/stream-limits.d.ts +15 -0
  41. package/node_modules/@agentclientprotocol/sdk/dist/stream-limits.js +58 -0
  42. package/node_modules/@agentclientprotocol/sdk/dist/stream-limits.js.map +1 -0
  43. package/node_modules/@agentclientprotocol/sdk/dist/stream.d.ts +4 -1
  44. package/node_modules/@agentclientprotocol/sdk/dist/stream.js +6 -2
  45. package/node_modules/@agentclientprotocol/sdk/dist/stream.js.map +1 -1
  46. package/node_modules/@agentclientprotocol/sdk/dist/test-support/until.d.ts +5 -0
  47. package/node_modules/@agentclientprotocol/sdk/dist/test-support/until.js +14 -0
  48. package/node_modules/@agentclientprotocol/sdk/dist/test-support/until.js.map +1 -0
  49. package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.d.ts +4 -1
  50. package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.js +3 -2
  51. package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.js.map +1 -1
  52. package/node_modules/@agentclientprotocol/sdk/dist/ws-server.js +143 -15
  53. package/node_modules/@agentclientprotocol/sdk/dist/ws-server.js.map +1 -1
  54. package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.d.ts +6 -0
  55. package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.js.map +1 -1
  56. package/node_modules/@agentclientprotocol/sdk/package.json +1 -1
  57. package/package.json +21 -21
  58. package/ui-dist/assets/{Agents.production-Cj5rZo69.js → Agents.production-Ou10IBgG.js} +2 -2
  59. package/ui-dist/assets/{CompanyActivity.production-BsesMNQp.js → CompanyActivity.production-dbz2o26S.js} +1 -1
  60. package/ui-dist/assets/{CompanyExport-CWmvCY4A.js → CompanyExport-VHEmaNC_.js} +1 -1
  61. package/ui-dist/assets/{CompanySkills.production-BGtfN-3A.js → CompanySkills.production-DuSUb8DQ.js} +1 -1
  62. package/ui-dist/assets/{Costs.production-84DycSrt.js → Costs.production-BRz8Rn3s.js} +1 -1
  63. package/ui-dist/assets/{OrgChart.production-D_VwOg1Z.js → OrgChart.production-Dgq5pMMa.js} +1 -1
  64. package/ui-dist/assets/{RoutineDetail.production-DJe0NpxJ.js → RoutineDetail.production-B2lkeY8y.js} +1 -1
  65. package/ui-dist/assets/{Routines.production-VhYuO012.js → Routines.production-CGNSsZDr.js} +1 -1
  66. package/ui-dist/assets/{definition-DurZnIaP.js → definition-D6jTm4Mv.js} +1 -1
  67. package/ui-dist/assets/{index-BqdSRbLj.js → index-DGStZYMS.js} +4 -4
  68. package/ui-dist/index.html +1 -1
  69. package/ui-dist/sw.js +1 -1
@@ -1,11 +1,24 @@
1
- import { serializeSseEvent, serializeSseKeepAlive } from "./sse.js";
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
- return {
42
- start(controller) {
43
- keepAliveTimer = setInterval(() => {
44
- if (isClosed || !hasDemand(controller)) {
45
- return;
46
- }
47
- if (!enqueueText(controller, serializeSseKeepAlive())) {
48
- closeBody(controller);
49
- }
50
- }, 15_000);
51
- },
52
- async pull(controller) {
53
- if (isClosed || isReceiving || !hasDemand(controller)) {
54
- return;
55
- }
56
- isReceiving = true;
57
- try {
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, serializeSseEvent(result.value))) {
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
- catch (error) {
71
- if (!isClosed) {
72
- isClosed = true;
73
- clearKeepAlive();
74
- controller.error(error);
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
- finally {
78
- isReceiving = false;
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,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAIpE,MAAM,UAAU,aAAa,CAC3B,KAAoB;IAEpB,OAAO,IAAI,cAAc,CAAa,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,gBAAgB;AAChB,MAAM,UAAU,mBAAmB,CACjC,KAAoB;IAEpB,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,cAA0D,CAAC;IAC/D,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,MAAM,cAAc,GAAG,GAAS,EAAE;QAChC,IAAI,cAAc,EAAE,CAAC;YACnB,aAAa,CAAC,cAAc,CAAC,CAAC;YAC9B,cAAc,GAAG,SAAS,CAAC;QAC7B,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,CAClB,UAAuD,EACvD,IAAY,EACH,EAAE;QACX,IAAI,CAAC;YACH,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YACzC,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,CAChB,UAAuD,EAC9C,EAAE,CAAC,UAAU,CAAC,WAAW,KAAK,IAAI,IAAI,UAAU,CAAC,WAAW,GAAG,CAAC,CAAC;IAE5E,MAAM,SAAS,GAAG,CAChB,UAAuD,EACjD,EAAE;QACR,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QAED,QAAQ,GAAG,IAAI,CAAC;QAChB,cAAc,EAAE,CAAC;QACjB,KAAK,CAAC,OAAO,EAAE,CAAC;QAEhB,IAAI,CAAC;YACH,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,mDAAmD;QACrD,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,UAAU;YACd,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE;gBAChC,IAAI,QAAQ,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC;oBACvC,OAAO;gBACT,CAAC;gBAED,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,qBAAqB,EAAE,CAAC,EAAE,CAAC;oBACtD,SAAS,CAAC,UAAU,CAAC,CAAC;gBACxB,CAAC;YACH,CAAC,EAAE,MAAM,CAAC,CAAC;QACb,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,UAAU;YACnB,IAAI,QAAQ,IAAI,WAAW,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC;gBACtD,OAAO;YACT,CAAC;YAED,WAAW,GAAG,IAAI,CAAC;YAEnB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC;gBAErC,IAAI,QAAQ,EAAE,CAAC;oBACb,OAAO;gBACT,CAAC;gBAED,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;oBAChB,SAAS,CAAC,UAAU,CAAC,CAAC;oBACtB,OAAO;gBACT,CAAC;gBAED,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;oBAC9D,SAAS,CAAC,UAAU,CAAC,CAAC;gBACxB,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACd,QAAQ,GAAG,IAAI,CAAC;oBAChB,cAAc,EAAE,CAAC;oBACjB,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC;oBAAS,CAAC;gBACT,WAAW,GAAG,KAAK,CAAC;YACtB,CAAC;QACH,CAAC;QACD,MAAM;YACJ,QAAQ,GAAG,IAAI,CAAC;YAChB,cAAc,EAAE,CAAC;YACjB,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC"}
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
- const pull = Promise.resolve(source.pull?.(controller));
38
+ await source.pull?.(controller);
39
39
  await flushMicrotasks();
40
40
  desiredSize = 0;
41
- mailbox.push(message);
42
- await pull;
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;AAE9C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAI7C,MAAM,OAAO,GAAG;IACd,OAAO,EAAE,KAAK;IACd,EAAE,EAAE,CAAC;IACL,MAAM,EAAE;QACN,EAAE,EAAE,IAAI;KACT;CACmB,CAAC;AAEvB,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,+EAA+E,EAAE,KAAK,IAAI,EAAE;QAC7F,MAAM,OAAO,GAAG,IAAI,eAAe,EAAc,CAAC;QAClD,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;QAC1C,MAAM,QAAQ,GAAiB,EAAE,CAAC;QAClC,IAAI,WAAW,GAAkB,CAAC,CAAC;QACnC,MAAM,UAAU,GAAG;YACjB,IAAI,WAAW;gBACb,OAAO,WAAW,CAAC;YACrB,CAAC;YACD,OAAO,CAAC,KAAiB;gBACvB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC;YACD,KAAK;gBACH,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,KAAK,CAAC,KAAe;gBACnB,IAAI,KAAK,EAAE,CAAC;oBACV,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;SAC6C,CAAC;QAEjD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;QACxD,MAAM,eAAe,EAAE,CAAC;QACxB,WAAW,GAAG,CAAC,CAAC;QAEhB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACtB,MAAM,IAAI,CAAC;QAEX,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,KAAK,UAAU,eAAe;IAC5B,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IACxB,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;AAC1B,CAAC;AAED,SAAS,UAAU,CAAC,KAAiB;IACnC,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzC,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(true);
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(true);
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
  }