@moltzap/client 2026.503.4 → 2026.504.1

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 (187) hide show
  1. package/dist/channel-core.d.ts +1 -4
  2. package/dist/channel-core.d.ts.map +1 -1
  3. package/dist/channel-core.js +0 -9
  4. package/dist/channel-core.js.map +1 -1
  5. package/dist/channel-core.test.js +90 -71
  6. package/dist/channel-core.test.js.map +1 -1
  7. package/dist/cli/commands/agents.js +2 -2
  8. package/dist/cli/commands/agents.js.map +1 -1
  9. package/dist/cli/commands/apps.d.ts +2 -24
  10. package/dist/cli/commands/apps.d.ts.map +1 -1
  11. package/dist/cli/commands/apps.js +22 -47
  12. package/dist/cli/commands/apps.js.map +1 -1
  13. package/dist/cli/commands/apps.test.js +59 -72
  14. package/dist/cli/commands/apps.test.js.map +1 -1
  15. package/dist/cli/commands/contacts.d.ts +1 -5
  16. package/dist/cli/commands/contacts.d.ts.map +1 -1
  17. package/dist/cli/commands/contacts.js +11 -26
  18. package/dist/cli/commands/contacts.js.map +1 -1
  19. package/dist/cli/commands/conversations-v2.test.js +30 -26
  20. package/dist/cli/commands/conversations-v2.test.js.map +1 -1
  21. package/dist/cli/commands/conversations.d.ts.map +1 -1
  22. package/dist/cli/commands/conversations.js +48 -33
  23. package/dist/cli/commands/conversations.js.map +1 -1
  24. package/dist/cli/commands/messages.d.ts.map +1 -1
  25. package/dist/cli/commands/messages.js +11 -8
  26. package/dist/cli/commands/messages.js.map +1 -1
  27. package/dist/cli/commands/messages.test.js +28 -30
  28. package/dist/cli/commands/messages.test.js.map +1 -1
  29. package/dist/cli/commands/presence.d.ts.map +1 -1
  30. package/dist/cli/commands/presence.js +5 -4
  31. package/dist/cli/commands/presence.js.map +1 -1
  32. package/dist/cli/commands/send.d.ts.map +1 -1
  33. package/dist/cli/commands/send.js +20 -11
  34. package/dist/cli/commands/send.js.map +1 -1
  35. package/dist/cli/commands/send.test.js +11 -9
  36. package/dist/cli/commands/send.test.js.map +1 -1
  37. package/dist/cli/commands/status.js +2 -2
  38. package/dist/cli/commands/status.js.map +1 -1
  39. package/dist/cli/commands/test-transport.d.ts +10 -0
  40. package/dist/cli/commands/test-transport.d.ts.map +1 -0
  41. package/dist/cli/commands/test-transport.js +29 -0
  42. package/dist/cli/commands/test-transport.js.map +1 -0
  43. package/dist/cli/index.d.ts.map +1 -1
  44. package/dist/cli/index.js +0 -2
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/cli/socket-client.d.ts +9 -4
  47. package/dist/cli/socket-client.d.ts.map +1 -1
  48. package/dist/cli/socket-client.js +27 -12
  49. package/dist/cli/socket-client.js.map +1 -1
  50. package/dist/cli/transport.d.ts +3 -2
  51. package/dist/cli/transport.d.ts.map +1 -1
  52. package/dist/cli/transport.js +6 -6
  53. package/dist/cli/transport.js.map +1 -1
  54. package/dist/cli/transport.test.js +4 -4
  55. package/dist/cli/transport.test.js.map +1 -1
  56. package/dist/index.d.ts +2 -3
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +1 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/internal/__tests__/app-callback-partition-key.test.d.ts +2 -0
  61. package/dist/internal/__tests__/app-callback-partition-key.test.d.ts.map +1 -0
  62. package/dist/internal/__tests__/app-callback-partition-key.test.js +33 -0
  63. package/dist/internal/__tests__/app-callback-partition-key.test.js.map +1 -0
  64. package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts +2 -0
  65. package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts.map +1 -0
  66. package/dist/internal/__tests__/{s2c-partition-worker.test.js → app-callback-partition-worker.test.js} +6 -9
  67. package/dist/internal/__tests__/app-callback-partition-worker.test.js.map +1 -0
  68. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts +2 -0
  69. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts.map +1 -0
  70. package/dist/internal/__tests__/{s2c-partitioned-dispatcher-real-ws.test.js → app-callback-partitioned-dispatcher-real-ws.test.js} +69 -94
  71. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.js.map +1 -0
  72. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts +2 -0
  73. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts.map +1 -0
  74. package/dist/internal/__tests__/{s2c-partitioned-dispatcher.test.js → app-callback-partitioned-dispatcher.test.js} +35 -69
  75. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.js.map +1 -0
  76. package/dist/internal/__tests__/app-callback-test-requests.d.ts +43 -0
  77. package/dist/internal/__tests__/app-callback-test-requests.d.ts.map +1 -0
  78. package/dist/internal/__tests__/app-callback-test-requests.js +101 -0
  79. package/dist/internal/__tests__/app-callback-test-requests.js.map +1 -0
  80. package/dist/internal/{s2c-dispatcher-errors.d.ts → app-callback-dispatcher-errors.d.ts} +5 -21
  81. package/dist/internal/app-callback-dispatcher-errors.d.ts.map +1 -0
  82. package/dist/internal/{s2c-dispatcher-errors.js → app-callback-dispatcher-errors.js} +3 -13
  83. package/dist/internal/app-callback-dispatcher-errors.js.map +1 -0
  84. package/dist/internal/app-callback-partition-key.d.ts +68 -0
  85. package/dist/internal/app-callback-partition-key.d.ts.map +1 -0
  86. package/dist/internal/app-callback-partition-key.js +90 -0
  87. package/dist/internal/app-callback-partition-key.js.map +1 -0
  88. package/dist/internal/{s2c-partition-worker.d.ts → app-callback-partition-worker.d.ts} +5 -5
  89. package/dist/internal/app-callback-partition-worker.d.ts.map +1 -0
  90. package/dist/internal/{s2c-partition-worker.js → app-callback-partition-worker.js} +5 -5
  91. package/dist/internal/app-callback-partition-worker.js.map +1 -0
  92. package/dist/internal/{s2c-partitioned-dispatcher.d.ts → app-callback-partitioned-dispatcher.d.ts} +9 -10
  93. package/dist/internal/app-callback-partitioned-dispatcher.d.ts.map +1 -0
  94. package/dist/internal/{s2c-partitioned-dispatcher.js → app-callback-partitioned-dispatcher.js} +12 -19
  95. package/dist/internal/app-callback-partitioned-dispatcher.js.map +1 -0
  96. package/dist/runtime/errors.d.ts +2 -28
  97. package/dist/runtime/errors.d.ts.map +1 -1
  98. package/dist/runtime/errors.js +0 -9
  99. package/dist/runtime/errors.js.map +1 -1
  100. package/dist/runtime/errors.test.js +2 -2
  101. package/dist/runtime/errors.test.js.map +1 -1
  102. package/dist/runtime/frame.d.ts +17 -18
  103. package/dist/runtime/frame.d.ts.map +1 -1
  104. package/dist/runtime/frame.js +83 -31
  105. package/dist/runtime/frame.js.map +1 -1
  106. package/dist/runtime/frame.test.js +15 -11
  107. package/dist/runtime/frame.test.js.map +1 -1
  108. package/dist/runtime/index.d.ts +1 -1
  109. package/dist/runtime/index.d.ts.map +1 -1
  110. package/dist/runtime/index.js +1 -1
  111. package/dist/runtime/index.js.map +1 -1
  112. package/dist/runtime/local-service-commands.d.ts +7 -0
  113. package/dist/runtime/local-service-commands.d.ts.map +1 -0
  114. package/dist/runtime/local-service-commands.js +6 -0
  115. package/dist/runtime/local-service-commands.js.map +1 -0
  116. package/dist/runtime/runtime.test.js +1 -2
  117. package/dist/runtime/runtime.test.js.map +1 -1
  118. package/dist/runtime/subscribers.d.ts +34 -29
  119. package/dist/runtime/subscribers.d.ts.map +1 -1
  120. package/dist/runtime/subscribers.js +19 -19
  121. package/dist/runtime/subscribers.js.map +1 -1
  122. package/dist/runtime/subscribers.test.js +50 -31
  123. package/dist/runtime/subscribers.test.js.map +1 -1
  124. package/dist/service.d.ts +24 -39
  125. package/dist/service.d.ts.map +1 -1
  126. package/dist/service.js +165 -161
  127. package/dist/service.js.map +1 -1
  128. package/dist/service.test.js +111 -170
  129. package/dist/service.test.js.map +1 -1
  130. package/dist/test-utils/channel-service-fixture.d.ts +1 -2
  131. package/dist/test-utils/channel-service-fixture.d.ts.map +1 -1
  132. package/dist/test-utils/channel-service-fixture.js +28 -20
  133. package/dist/test-utils/channel-service-fixture.js.map +1 -1
  134. package/dist/test-utils/conformance-adapter.d.ts +2 -2
  135. package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
  136. package/dist/test-utils/conformance-adapter.js +33 -26
  137. package/dist/test-utils/conformance-adapter.js.map +1 -1
  138. package/dist/test-utils/fake-service.d.ts +14 -39
  139. package/dist/test-utils/fake-service.d.ts.map +1 -1
  140. package/dist/test-utils/fake-service.js +33 -39
  141. package/dist/test-utils/fake-service.js.map +1 -1
  142. package/dist/test-utils/ids.d.ts +9 -0
  143. package/dist/test-utils/ids.d.ts.map +1 -0
  144. package/dist/test-utils/ids.js +52 -0
  145. package/dist/test-utils/ids.js.map +1 -0
  146. package/dist/test-utils/index.d.ts +10 -2
  147. package/dist/test-utils/index.d.ts.map +1 -1
  148. package/dist/test-utils/index.js +12 -5
  149. package/dist/test-utils/index.js.map +1 -1
  150. package/dist/ws-client.d.ts +58 -64
  151. package/dist/ws-client.d.ts.map +1 -1
  152. package/dist/ws-client.js +151 -133
  153. package/dist/ws-client.js.map +1 -1
  154. package/dist/ws-client.test.js +157 -167
  155. package/dist/ws-client.test.js.map +1 -1
  156. package/package.json +3 -3
  157. package/dist/cli/commands/permissions.d.ts +0 -70
  158. package/dist/cli/commands/permissions.d.ts.map +0 -1
  159. package/dist/cli/commands/permissions.js +0 -100
  160. package/dist/cli/commands/permissions.js.map +0 -1
  161. package/dist/cli/commands/permissions.test.d.ts +0 -2
  162. package/dist/cli/commands/permissions.test.d.ts.map +0 -1
  163. package/dist/cli/commands/permissions.test.js +0 -127
  164. package/dist/cli/commands/permissions.test.js.map +0 -1
  165. package/dist/internal/__tests__/s2c-partition-key.test.d.ts +0 -2
  166. package/dist/internal/__tests__/s2c-partition-key.test.d.ts.map +0 -1
  167. package/dist/internal/__tests__/s2c-partition-key.test.js +0 -231
  168. package/dist/internal/__tests__/s2c-partition-key.test.js.map +0 -1
  169. package/dist/internal/__tests__/s2c-partition-worker.test.d.ts +0 -2
  170. package/dist/internal/__tests__/s2c-partition-worker.test.d.ts.map +0 -1
  171. package/dist/internal/__tests__/s2c-partition-worker.test.js.map +0 -1
  172. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts +0 -2
  173. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts.map +0 -1
  174. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.js.map +0 -1
  175. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts +0 -2
  176. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts.map +0 -1
  177. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.js.map +0 -1
  178. package/dist/internal/s2c-dispatcher-errors.d.ts.map +0 -1
  179. package/dist/internal/s2c-dispatcher-errors.js.map +0 -1
  180. package/dist/internal/s2c-partition-key.d.ts +0 -79
  181. package/dist/internal/s2c-partition-key.d.ts.map +0 -1
  182. package/dist/internal/s2c-partition-key.js +0 -146
  183. package/dist/internal/s2c-partition-key.js.map +0 -1
  184. package/dist/internal/s2c-partition-worker.d.ts.map +0 -1
  185. package/dist/internal/s2c-partition-worker.js.map +0 -1
  186. package/dist/internal/s2c-partitioned-dispatcher.d.ts.map +0 -1
  187. package/dist/internal/s2c-partitioned-dispatcher.js.map +0 -1
@@ -20,12 +20,44 @@ import { it as itEffect } from "@effect/vitest";
20
20
  import { Cause, Duration, Effect, Exit, Fiber, Option, Scope, TestClock, } from "effect";
21
21
  import * as NodeSocketServer from "@effect/platform-node/NodeSocketServer";
22
22
  import * as Socket from "@effect/platform/Socket";
23
- import { PROTOCOL_VERSION } from "@moltzap/protocol";
24
23
  import { MoltZapWsClient, RPC_TIMEOUT_MS, } from "./ws-client.js";
25
- import { RpcServerError, RpcTimeoutError } from "./runtime/errors.js";
26
- import { AgentsLookupByName, AppsOnClose, AppsOnJoin, Connect, ConversationsList, MessagesSend, } from "@moltzap/protocol";
24
+ import { RpcServerError, RpcTimeoutError } from "@moltzap/protocol";
25
+ import { AppsOnClose, AppsOnSessionActive, Connect, ConversationsList, JSON_RPC_VERSION, MessagesSend, MessageReceivedNotificationDefinition, PROTOCOL_VERSION, agentId, conversationId, jsonRpcStringId, messageId, notificationFrame, requestFrame, responseFrame, validators, } from "@moltzap/protocol";
26
+ import { onCloseParams, onSessionActiveParams, SESSION_A, SESSION_B, } from "./internal/__tests__/app-callback-test-requests.js";
27
27
  // ── Test server helpers ────────────────────────────────────────────────
28
28
  const LOCALHOST_HOST = "127.0.0.1";
29
+ const TEST_AGENT_ID = agentId("11111111-1111-4111-8111-111111111111");
30
+ const RECONNECT_AGENT_ID = agentId("22222222-2222-4222-8222-222222222222");
31
+ const TEST_MESSAGE_ID = messageId("44444444-4444-4444-8444-444444444444");
32
+ const TEST_CONVERSATION_ID = conversationId("33333333-3333-4333-8333-333333333333");
33
+ const TEST_POLICY = {
34
+ maxMessageBytes: 1_000_000,
35
+ maxPartsPerMessage: 10,
36
+ maxTextLength: 32_768,
37
+ maxGroupParticipants: 100,
38
+ heartbeatIntervalMs: 30_000,
39
+ rateLimits: {
40
+ messagesPerMinute: 60,
41
+ requestsPerMinute: 120,
42
+ },
43
+ };
44
+ const helloOk = (agentId = TEST_AGENT_ID) => ({
45
+ protocolVersion: PROTOCOL_VERSION,
46
+ agentId,
47
+ conversations: [],
48
+ unreadCounts: {},
49
+ policy: TEST_POLICY,
50
+ });
51
+ const TEST_MESSAGE = {
52
+ id: TEST_MESSAGE_ID,
53
+ conversationId: TEST_CONVERSATION_ID,
54
+ senderId: TEST_AGENT_ID,
55
+ parts: [{ type: "text", text: "hello" }],
56
+ createdAt: "2026-05-03T00:00:00.000Z",
57
+ };
58
+ const messageReceivedFrame = () => notificationFrame(MessageReceivedNotificationDefinition, {
59
+ message: TEST_MESSAGE,
60
+ });
29
61
  /**
30
62
  * Spin up an in-process `@effect/platform` WS server on `127.0.0.1:0`.
31
63
  * Caller owns the provided scope; when it closes, the server shuts down.
@@ -96,7 +128,7 @@ function makeLogger() {
96
128
  };
97
129
  }
98
130
  const connectP = (client) => Effect.runPromise(client.connect().pipe(Effect.catchTag("RpcTimeoutError", (err) => Effect.fail(new Error(`RPC timeout: ${err.method}`))), Effect.catchAll((err) => Effect.fail(new Error(err.message)))));
99
- const sendRpcP = (client, method, params) => Effect.runPromise(client.sendRpc(method, params).pipe(Effect.catchTag("RpcTimeoutError", (err) => Effect.fail(new Error(`RPC timeout: ${err.method}`))), Effect.catchAll((err) => Effect.fail(new Error(err.message)))));
131
+ const sendRpcP = (client, definition, params) => Effect.runPromise(client.sendRpc(definition, params).pipe(Effect.catchTag("RpcTimeoutError", (err) => Effect.fail(new Error(`RPC timeout: ${err.method}`))), Effect.catchAll((err) => Effect.fail(new Error(err.message)))));
100
132
  const closeClient = (client) => client.close();
101
133
  /**
102
134
  * Start a server whose handler auto-responds to `auth/connect` and forwards
@@ -104,15 +136,15 @@ const closeClient = (client) => client.close();
104
136
  * about post-handshake behaviour.
105
137
  */
106
138
  const startHandshakingServer = (handler) => startTestServer((conn, raw) => Effect.gen(function* () {
107
- const frame = JSON.parse(raw);
139
+ const parsed = JSON.parse(raw);
140
+ if (!validators.requestFrame(parsed)) {
141
+ return yield* Effect.die("expected JSON-RPC request frame");
142
+ }
143
+ const frame = parsed;
108
144
  if (frame.method === Connect.name) {
109
- yield* conn.send(JSON.stringify({
110
- jsonrpc: "2.0",
111
- type: "response",
112
- direction: "c2s",
113
- id: frame.id,
114
- result: { agentId: "agent-xyz", protocol: PROTOCOL_VERSION },
115
- }));
145
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
146
+ result: helloOk(),
147
+ })));
116
148
  return;
117
149
  }
118
150
  yield* handler(conn, raw, frame);
@@ -147,10 +179,10 @@ async function waitFor(pred, { maxMs = 2000 } = {}) {
147
179
  }
148
180
  }
149
181
  /**
150
- * Build a `MoltZapWsClient`. Spec #222 OQ-4 deletion: `onEvent` is no
182
+ * Build a `MoltZapWsClient`. Spec #222 OQ-4 deletion: `onNotification` is no
151
183
  * longer a constructor option; tests that previously stashed an
152
- * `onEvent` callback now register a `subscribe({}, …)` subscription
153
- * post-construction. The helper accepts the same `onEvent` callback as
184
+ * `onNotification` callback now register a `subscribe({}, …)` subscription
185
+ * post-construction. The helper accepts the same `onNotification` callback as
154
186
  * a convenience and wires it through the new subscription registry,
155
187
  * keeping migration noise local to the helper.
156
188
  */
@@ -162,8 +194,8 @@ function makeClient(url, overrides) {
162
194
  onReconnect: overrides?.onReconnect ?? (() => { }),
163
195
  logger: overrides?.logger ?? makeLogger(),
164
196
  });
165
- if (overrides?.onEvent !== undefined) {
166
- const cb = overrides.onEvent;
197
+ if (overrides?.onNotification !== undefined) {
198
+ const cb = overrides.onNotification;
167
199
  Effect.runSync(client.subscribe({}, (frame) => Effect.sync(() => cb(frame))));
168
200
  }
169
201
  return client;
@@ -220,7 +252,7 @@ describe("§5.1 connect() does not hang on pre-open failure", () => {
220
252
  const server = yield* startHandshakingServer(() => Effect.void);
221
253
  const client = makeClient(server.url);
222
254
  const hello = (yield* Effect.promise(() => connectP(client)));
223
- expect(hello.agentId).toBe("agent-xyz");
255
+ expect(hello.agentId).toBe(TEST_AGENT_ID);
224
256
  expect(client.helloOk).toEqual(hello);
225
257
  yield* closeClient(client);
226
258
  }));
@@ -237,8 +269,8 @@ describe("§5.2 pending RPCs fail on disconnect", () => {
237
269
  const server = yield* startHandshakingServer(() => Effect.void);
238
270
  const client = makeClient(server.url);
239
271
  yield* Effect.promise(() => connectP(client));
240
- const rpcP = sendRpcP(client, MessagesSend.name, {
241
- conversationId: "c1",
272
+ const rpcP = sendRpcP(client, MessagesSend, {
273
+ conversationId: TEST_CONVERSATION_ID,
242
274
  parts: [{ type: "text", text: "hi" }],
243
275
  });
244
276
  // Wait for the RPC frame to land on the server.
@@ -275,8 +307,8 @@ describe("§5.3 sendRpc does NOT retry on timeout (TestClock)", () => {
275
307
  yield* Effect.promise(() => connectP(client));
276
308
  const serverConn = server.connections[0];
277
309
  const beforeCount = serverConn.received.length;
278
- const rpcFiber = yield* Effect.fork(client.sendRpc(MessagesSend.name, {
279
- conversationId: "c1",
310
+ const rpcFiber = yield* Effect.fork(client.sendRpc(MessagesSend, {
311
+ conversationId: TEST_CONVERSATION_ID,
280
312
  parts: [{ type: "text", text: "payload" }],
281
313
  }));
282
314
  // Wait for the frame to land on the server using real-time polling.
@@ -342,16 +374,16 @@ describe("reconnect backoff", () => {
342
374
  let authResponsesSent = 0;
343
375
  let reconnectHello = null;
344
376
  const server = yield* startTestServer((conn, raw) => Effect.gen(function* () {
345
- const frame = JSON.parse(raw);
377
+ const parsed = JSON.parse(raw);
378
+ if (!validators.requestFrame(parsed)) {
379
+ return yield* Effect.die("expected JSON-RPC request frame");
380
+ }
381
+ const frame = parsed;
346
382
  if (frame.method === Connect.name) {
347
383
  authResponsesSent++;
348
- yield* conn.send(JSON.stringify({
349
- jsonrpc: "2.0",
350
- type: "response",
351
- direction: "c2s",
352
- id: frame.id,
353
- result: { agentId: "agent-1" },
354
- }));
384
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
385
+ result: helloOk(RECONNECT_AGENT_ID),
386
+ })));
355
387
  }
356
388
  }));
357
389
  const client = makeClient(server.url, {
@@ -370,7 +402,7 @@ describe("reconnect backoff", () => {
370
402
  expect(server.connections.length).toBeGreaterThanOrEqual(2);
371
403
  expect(authResponsesSent).toBeGreaterThanOrEqual(2);
372
404
  yield* Effect.promise(() => waitFor(() => reconnectHello !== null, { maxMs: 500 }));
373
- expect(reconnectHello.agentId).toBe("agent-1");
405
+ expect(reconnectHello.agentId).toBe(RECONNECT_AGENT_ID);
374
406
  yield* closeClient(client);
375
407
  }));
376
408
  });
@@ -387,101 +419,85 @@ describe("§5.4 malformed frames are logged but do not affect pending RPCs", ()
387
419
  const server = yield* startHandshakingServer((conn, _raw, frame) => Effect.gen(function* () {
388
420
  if (frame.method !== ConversationsList.name)
389
421
  return;
390
- // Inject: non-JSON, then missing-id response, then unknown type.
422
+ // Inject: non-JSON, then a missing-id response-like frame, then
423
+ // an unknown object shape.
391
424
  yield* conn.send("not json at all");
392
- yield* conn.send(JSON.stringify({ type: "response" }));
393
- yield* conn.send(JSON.stringify({ type: "unknown", id: frame.id }));
425
+ yield* conn.send(JSON.stringify({ jsonrpc: JSON_RPC_VERSION, result: {} }));
426
+ yield* conn.send(JSON.stringify({ jsonrpc: JSON_RPC_VERSION, id: frame.id }));
394
427
  // Real well-formed response.
395
- yield* conn.send(JSON.stringify({
396
- jsonrpc: "2.0",
397
- type: "response",
398
- direction: "c2s",
399
- id: frame.id,
428
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
400
429
  result: { conversations: [] },
401
- }));
430
+ })));
402
431
  }));
403
432
  const client = makeClient(server.url, { logger });
404
433
  yield* Effect.promise(() => connectP(client));
405
- const result = (yield* Effect.promise(() => sendRpcP(client, ConversationsList.name, {})));
434
+ const result = (yield* Effect.promise(() => sendRpcP(client, ConversationsList, {})));
406
435
  expect(result.conversations).toEqual([]);
407
436
  // Logger saw at least one malformed-frame warning.
408
437
  expect(logger.warn).toHaveBeenCalled();
409
438
  yield* closeClient(client);
410
439
  }));
411
440
  });
412
- it("accepts a padded chunk that contains both an event and the response", async () => {
441
+ it("accepts a padded chunk that contains both a notification and the response", async () => {
413
442
  await withTestServer(Effect.gen(function* () {
414
443
  const logger = makeLogger();
415
444
  const events = [];
416
445
  const server = yield* startHandshakingServer((conn, _raw, frame) => Effect.gen(function* () {
417
446
  if (frame.method !== ConversationsList.name)
418
447
  return;
419
- yield* conn.send(JSON.stringify({
420
- jsonrpc: "2.0",
421
- type: "event",
422
- event: "messages/received",
423
- data: { message: { id: "m-1", conversationId: "c-1" } },
424
- }) +
448
+ yield* conn.send(JSON.stringify(messageReceivedFrame()) +
425
449
  "\u0000" +
426
- JSON.stringify({
427
- jsonrpc: "2.0",
428
- type: "response",
429
- direction: "c2s",
430
- id: frame.id,
450
+ JSON.stringify(responseFrame(frame.id, {
431
451
  result: { conversations: [] },
432
- }));
452
+ })));
433
453
  }));
434
454
  const client = makeClient(server.url, {
435
455
  logger,
436
- onEvent: (event) => events.push(event),
456
+ onNotification: (event) => events.push(event),
437
457
  });
438
458
  yield* Effect.promise(() => connectP(client));
439
- const result = (yield* Effect.promise(() => sendRpcP(client, ConversationsList.name, {})));
459
+ const result = (yield* Effect.promise(() => sendRpcP(client, ConversationsList, {})));
440
460
  expect(result.conversations).toEqual([]);
441
461
  expect(events).toHaveLength(1);
442
- expect(events[0]).toMatchObject({ event: "messages/received" });
462
+ expect(events[0]).toMatchObject({
463
+ method: MessageReceivedNotificationDefinition.name,
464
+ });
443
465
  expect(logger.warn).not.toHaveBeenCalled();
444
466
  yield* closeClient(client);
445
467
  }));
446
468
  });
447
- it("routes a well-formed event frame to onEvent", async () => {
469
+ it("routes a well-formed notification frame to onNotification", async () => {
448
470
  await withTestServer(Effect.gen(function* () {
449
471
  const events = [];
450
- const server = yield* startHandshakingServer((conn) => conn.send(JSON.stringify({
451
- jsonrpc: "2.0",
452
- type: "event",
453
- event: "message.received",
454
- data: { message: { id: "m-1", conversationId: "c-1" } },
455
- })));
472
+ const server = yield* startHandshakingServer((conn) => conn.send(JSON.stringify(messageReceivedFrame())));
456
473
  const client = makeClient(server.url, {
457
- onEvent: (e) => events.push(e),
474
+ onNotification: (e) => events.push(e),
458
475
  });
459
476
  yield* Effect.promise(() => connectP(client));
460
- // Fire-and-forget: the server responds with an out-of-band event
477
+ // Fire-and-forget: the server responds with an out-of-band notification
461
478
  // rather than an RPC response, so the noop Deferred never resolves.
462
479
  // Awaiting it would wedge the test for the full RPC_TIMEOUT_MS.
463
- void sendRpcP(client, "noop", {}).catch(() => { });
480
+ void sendRpcP(client, ConversationsList, {}).catch(() => { });
464
481
  yield* Effect.promise(() => waitFor(() => events.length > 0, { maxMs: 2000 }));
465
482
  expect(events[0]).toMatchObject({
466
- type: "event",
467
- event: "message.received",
483
+ method: MessageReceivedNotificationDefinition.name,
468
484
  });
469
485
  yield* closeClient(client);
470
486
  }));
471
487
  });
472
- it("does NOT route an event frame missing the event name field", async () => {
488
+ it("does NOT route a notification frame missing the method field", async () => {
473
489
  await withTestServer(Effect.gen(function* () {
474
490
  const events = [];
475
491
  const logger = makeLogger();
476
- // Send a malformed event on the first post-handshake frame.
477
- const server = yield* startHandshakingServer((conn) => conn.send(JSON.stringify({ type: "event", data: {} })));
492
+ // Send a malformed notification on the first post-handshake frame.
493
+ const server = yield* startHandshakingServer((conn) => conn.send(JSON.stringify({ jsonrpc: JSON_RPC_VERSION, params: {} })));
478
494
  const client = makeClient(server.url, {
479
- onEvent: (e) => events.push(e),
495
+ onNotification: (e) => events.push(e),
480
496
  logger,
481
497
  });
482
498
  yield* Effect.promise(() => connectP(client));
483
499
  // Fire-and-forget: see the well-formed-event test above for rationale.
484
- void sendRpcP(client, "noop", {}).catch(() => { });
500
+ void sendRpcP(client, ConversationsList, {}).catch(() => { });
485
501
  yield* Effect.promise(() => new Promise((r) => setTimeout(r, 50)));
486
502
  expect(events).toHaveLength(0);
487
503
  expect(logger.warn).toHaveBeenCalled();
@@ -508,7 +524,7 @@ describe("malformed-frame log cadence (MALFORMED_LOG_EVERY)", () => {
508
524
  logger.warn.mockClear();
509
525
  // Fire-and-forget: server responds with 101 malformed frames, no
510
526
  // actual RPC response, so awaiting the noop would wedge the test.
511
- void sendRpcP(client, "noop", {}).catch(() => { });
527
+ void sendRpcP(client, ConversationsList, {}).catch(() => { });
512
528
  // Wait for the malformed frames to flush through the reader fiber.
513
529
  yield* Effect.promise(() => new Promise((r) => setTimeout(r, 300)));
514
530
  expect(logger.warn).toHaveBeenCalledTimes(3);
@@ -530,7 +546,7 @@ describe("close() interleaved with a pending RPC", () => {
530
546
  const server = yield* startHandshakingServer(() => Effect.void);
531
547
  const client = makeClient(server.url);
532
548
  yield* Effect.promise(() => connectP(client));
533
- const rpcP = sendRpcP(client, ConversationsList.name, {});
549
+ const rpcP = sendRpcP(client, ConversationsList, {});
534
550
  yield* Effect.promise(() => waitFor(() => server.connections[0].received.length >= 2));
535
551
  const beforeMs = Date.now();
536
552
  yield* closeClient(client);
@@ -550,7 +566,7 @@ describe("socket error after connect", () => {
550
566
  const server = yield* startHandshakingServer((conn) => conn.close(1011));
551
567
  const client = makeClient(server.url, { logger });
552
568
  yield* Effect.promise(() => connectP(client));
553
- const rpcP = sendRpcP(client, ConversationsList.name, {});
569
+ const rpcP = sendRpcP(client, ConversationsList, {});
554
570
  yield* Effect.promise(() => expect(rpcP).rejects.toThrow(/WebSocket not connected/));
555
571
  // Logger captured the WebSocket error (warn level).
556
572
  expect(logger.warn).toHaveBeenCalled();
@@ -565,40 +581,19 @@ describe("sendRpc(RpcDefinition, params) — typed manifest overload", () => {
565
581
  it("uses definition.name as the wire-level method string", async () => {
566
582
  await withTestServer(Effect.gen(function* () {
567
583
  const { AgentsLookupByName } = yield* Effect.promise(() => import("@moltzap/protocol"));
568
- // Echo the method name + params back as the result so the test can
569
- // verify both the wire-level method and the forwarded params.
570
- const server = yield* startHandshakingServer((conn, _raw, frame) => conn.send(JSON.stringify({
571
- jsonrpc: "2.0",
572
- type: "response",
573
- direction: "c2s",
574
- id: frame.id,
575
- result: {
576
- echoedMethod: frame.method,
577
- echoedParams: frame.params,
578
- agents: [],
579
- },
580
- })));
581
- const client = makeClient(server.url);
582
- yield* Effect.promise(() => connectP(client));
583
- const result = (yield* Effect.promise(() => Effect.runPromise(client.sendRpc(AgentsLookupByName, { names: ["alice"] }))));
584
- expect(result.echoedMethod).toBe(AgentsLookupByName.name);
585
- expect(result.echoedParams).toEqual({ names: ["alice"] });
586
- yield* closeClient(client);
587
- }));
588
- });
589
- it("preserves the legacy string overload for back-compat", async () => {
590
- await withTestServer(Effect.gen(function* () {
591
- const server = yield* startHandshakingServer((conn, _raw, frame) => conn.send(JSON.stringify({
592
- jsonrpc: "2.0",
593
- type: "response",
594
- direction: "c2s",
595
- id: frame.id,
596
- result: { echoedMethod: frame.method, agents: [] },
597
- })));
584
+ const captured = { current: null };
585
+ const server = yield* startHandshakingServer((conn, _raw, frame) => Effect.gen(function* () {
586
+ captured.current = frame;
587
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
588
+ result: { agents: [] },
589
+ })));
590
+ }));
598
591
  const client = makeClient(server.url);
599
592
  yield* Effect.promise(() => connectP(client));
600
- const result = (yield* Effect.promise(() => sendRpcP(client, AgentsLookupByName.name, { names: ["bob"] })));
601
- expect(result.echoedMethod).toBe(AgentsLookupByName.name);
593
+ const result = yield* Effect.promise(() => Effect.runPromise(client.sendRpc(AgentsLookupByName, { names: ["alice"] })));
594
+ expect(result.agents).toEqual([]);
595
+ expect(captured.current?.method).toBe(AgentsLookupByName.name);
596
+ expect(captured.current?.params).toEqual({ names: ["alice"] });
602
597
  yield* closeClient(client);
603
598
  }));
604
599
  });
@@ -697,55 +692,49 @@ describe("spec #222 §5.4 — onDisconnect close metadata (V7)", () => {
697
692
  });
698
693
  // ─────────────────────────────────────────────────────────────────────
699
694
  // Phase 1.0 (B.1) gating tests — client-side server-initiated RPC
700
- // (handleServerRpc + dispatcher fiber + s2c response write-back)
695
+ // (handleServerRpc + dispatcher fiber + appCallback response write-back)
701
696
  // ─────────────────────────────────────────────────────────────────────
702
697
  describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
703
- it("dispatches an inbound s2c request to the registered handler and writes the response back", async () => {
698
+ it("dispatches an inbound appCallback request to the registered handler and writes the response back", async () => {
704
699
  await withTestServer(Effect.gen(function* () {
705
700
  // Server: auto-handshake; immediately AFTER replying to
706
- // auth/connect, send an s2c request to the client; capture every
701
+ // auth/connect, send an appCallback request to the client; capture every
707
702
  // subsequent inbound frame the client writes back. We're testing
708
703
  // the client's dispatcher fiber + handler registry + response
709
704
  // encoding.
710
705
  const server = yield* startTestServer((conn, raw) => Effect.gen(function* () {
711
- const frame = JSON.parse(raw);
706
+ const parsed = JSON.parse(raw);
707
+ if (!validators.requestFrame(parsed)) {
708
+ return yield* Effect.die("expected JSON-RPC request frame");
709
+ }
710
+ const frame = parsed;
712
711
  if (frame.method === Connect.name) {
713
- yield* conn.send(JSON.stringify({
714
- jsonrpc: "2.0",
715
- type: "response",
716
- direction: "c2s",
717
- id: frame.id,
718
- result: { agentId: "agent-xyz", protocol: PROTOCOL_VERSION },
719
- }));
720
- // Fire the s2c request straight after auth response. The
712
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
713
+ result: helloOk(),
714
+ })));
715
+ // Fire the appCallback request straight after auth response. The
721
716
  // client's dispatcher fiber was forked into the connect
722
717
  // scope BEFORE auth/connect was sent, so the inbound queue
723
718
  // is live the moment we land here.
724
- yield* conn.send(JSON.stringify({
725
- jsonrpc: "2.0",
726
- type: "request",
727
- direction: "s2c",
728
- id: "srv-test-1",
729
- method: AppsOnJoin.name,
730
- params: { sessionId: "sess-A" },
731
- }));
719
+ yield* conn.send(JSON.stringify(requestFrame(jsonRpcStringId("srv-test-1"), AppsOnSessionActive, onSessionActiveParams(SESSION_A))));
732
720
  }
733
721
  }));
734
722
  const client = makeClient(server.url);
735
723
  // Register a handler BEFORE connect so the dispatcher fiber sees
736
- // it on the very first inbound s2c request.
737
- yield* client.handleServerRpc(AppsOnJoin.name, (params) => Effect.succeed({
724
+ // it on the very first inbound appCallback request.
725
+ yield* client.handleServerRpc(AppsOnSessionActive, (params) => Effect.succeed({
738
726
  ack: true,
739
727
  saw: params.sessionId,
740
728
  }));
741
729
  yield* Effect.promise(() => connectP(client));
742
730
  // Wait for the response frame the dispatcher writes back. The
743
- // server records every inbound frame in `received` — the s2c
731
+ // server records every inbound frame in `received` — the appCallback
744
732
  // response should appear after the client's auth/connect.
745
733
  const responseRaw = yield* Effect.promise(() => waitFor(() => server.connections[0].received.some((r) => {
746
734
  try {
747
735
  const parsed = JSON.parse(r);
748
- return (parsed.type === "response" && parsed.id === "srv-test-1");
736
+ return (validators.responseFrame(parsed) &&
737
+ parsed.id === "srv-test-1");
749
738
  }
750
739
  catch {
751
740
  return false;
@@ -753,51 +742,44 @@ describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
753
742
  }), { maxMs: 2000 })).pipe(Effect.flatMap(() => Effect.sync(() => server.connections[0].received.find((r) => {
754
743
  try {
755
744
  const parsed = JSON.parse(r);
756
- return parsed.id === "srv-test-1";
745
+ return (validators.responseFrame(parsed) &&
746
+ parsed.id === "srv-test-1");
757
747
  }
758
748
  catch {
759
749
  return false;
760
750
  }
761
751
  }))));
762
752
  const parsedResponse = JSON.parse(responseRaw);
763
- expect(parsedResponse.type).toBe("response");
764
- expect(parsedResponse.direction).toBe("s2c");
753
+ expect(validators.responseFrame(parsedResponse)).toBe(true);
754
+ if (!validators.responseFrame(parsedResponse))
755
+ return;
765
756
  expect(parsedResponse.id).toBe("srv-test-1");
766
- expect(parsedResponse.result.ack).toBe(true);
767
- expect(parsedResponse.result.saw).toBe("sess-A");
757
+ expect("result" in parsedResponse).toBe(true);
758
+ if (!("result" in parsedResponse))
759
+ return;
760
+ const result = parsedResponse.result;
761
+ expect(result.ack).toBe(true);
762
+ expect(result.saw).toBe(SESSION_A);
768
763
  yield* closeClient(client);
769
764
  }));
770
765
  });
771
766
  it("encodes a typed RpcServerError from the handler as a `response` frame with `error`", async () => {
772
767
  await withTestServer(Effect.gen(function* () {
773
768
  const server = yield* startTestServer((conn, raw) => Effect.gen(function* () {
774
- const frame = JSON.parse(raw);
769
+ const parsed = JSON.parse(raw);
770
+ if (!validators.requestFrame(parsed)) {
771
+ return yield* Effect.die("expected JSON-RPC request frame");
772
+ }
773
+ const frame = parsed;
775
774
  if (frame.method === Connect.name) {
776
- yield* conn.send(JSON.stringify({
777
- jsonrpc: "2.0",
778
- type: "response",
779
- direction: "c2s",
780
- id: frame.id,
781
- result: { agentId: "agent-xyz", protocol: PROTOCOL_VERSION },
782
- }));
783
- yield* conn.send(JSON.stringify({
784
- jsonrpc: "2.0",
785
- type: "request",
786
- direction: "s2c",
787
- id: "srv-err-1",
788
- method: AppsOnClose.name,
789
- // Spec #356: the partitioned dispatcher
790
- // narrow-validates routing fields; sessionId is
791
- // required for every s2c hook. Synthetic test
792
- // payloads must include it or the dispatcher
793
- // rejects the offer with a `-32602` wire error
794
- // before reaching the handler.
795
- params: { sessionId: "sess-err-1" },
796
- }));
775
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
776
+ result: helloOk(),
777
+ })));
778
+ yield* conn.send(JSON.stringify(requestFrame(jsonRpcStringId("srv-err-1"), AppsOnClose, onCloseParams(SESSION_B))));
797
779
  }
798
780
  }));
799
781
  const client = makeClient(server.url);
800
- yield* client.handleServerRpc(AppsOnClose.name, () => Effect.fail(new RpcServerError({
782
+ yield* client.handleServerRpc(AppsOnClose, () => Effect.fail(new RpcServerError({
801
783
  code: -32099,
802
784
  message: "domain-rejected",
803
785
  data: { reason: "test" },
@@ -806,7 +788,9 @@ describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
806
788
  yield* Effect.promise(() => waitFor(() => server.connections[0].received.some((r) => {
807
789
  try {
808
790
  const parsed = JSON.parse(r);
809
- return (parsed.id === "srv-err-1" && parsed.error !== undefined);
791
+ return (validators.responseFrame(parsed) &&
792
+ parsed.id === "srv-err-1" &&
793
+ "error" in parsed);
810
794
  }
811
795
  catch {
812
796
  return false;
@@ -814,14 +798,20 @@ describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
814
798
  }), { maxMs: 2000 }));
815
799
  const found = server.connections[0].received.find((r) => {
816
800
  try {
817
- return JSON.parse(r).id === "srv-err-1";
801
+ const parsed = JSON.parse(r);
802
+ return (validators.responseFrame(parsed) && parsed.id === "srv-err-1");
818
803
  }
819
804
  catch {
820
805
  return false;
821
806
  }
822
807
  });
823
808
  const parsed = JSON.parse(found);
824
- expect(parsed.direction).toBe("s2c");
809
+ expect(validators.responseFrame(parsed)).toBe(true);
810
+ if (!validators.responseFrame(parsed))
811
+ return;
812
+ expect("error" in parsed).toBe(true);
813
+ if (!("error" in parsed))
814
+ return;
825
815
  expect(parsed.error.code).toBe(-32099);
826
816
  expect(parsed.error.message).toBe("domain-rejected");
827
817
  expect(parsed.error.data.reason).toBe("test");
@@ -833,9 +823,9 @@ describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
833
823
  serverUrl: "http://127.0.0.1:1",
834
824
  agentKey: "test",
835
825
  });
836
- const first = await Effect.runPromiseExit(client.handleServerRpc(AppsOnJoin.name, () => Effect.succeed({})));
826
+ const first = await Effect.runPromiseExit(client.handleServerRpc(AppsOnSessionActive, () => Effect.succeed({})));
837
827
  expect(Exit.isSuccess(first)).toBe(true);
838
- const second = await Effect.runPromiseExit(client.handleServerRpc(AppsOnJoin.name, () => Effect.succeed({})));
828
+ const second = await Effect.runPromiseExit(client.handleServerRpc(AppsOnSessionActive, () => Effect.succeed({})));
839
829
  expect(Exit.isFailure(second)).toBe(true);
840
830
  if (Exit.isFailure(second)) {
841
831
  const err = Cause.failureOption(second.cause);
@@ -850,7 +840,7 @@ describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
850
840
  // ─────────────────────────────────────────────────────────────────────
851
841
  // Regression gate (review-295): runSync(client.close()) and
852
842
  // runSync(client.disconnect()) must not throw AsyncFiberException after
853
- // the s2c queue + dispatcher were added. The s2c machinery lives off the
843
+ // the appCallback queue + dispatcher were added. The appCallback machinery lives off the
854
844
  // per-connect Scope (allocated inline + forked via runtime.runFork) so
855
845
  // `Scope.close` stays sync; teardown of the queue and dispatcher fiber
856
846
  // is dispatched via `runFork` from close()/disconnectSync().