@moltzap/client 2026.503.4 → 2026.504.0

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 +58 -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 +124 -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 +86 -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
package/dist/service.js CHANGED
@@ -2,11 +2,12 @@ import * as net from "node:net";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import * as os from "node:os";
5
- import { AgentsLookup, AgentsLookupByName, AppsAuthorizeDispatch, ConversationsCreate, ConversationsGet, ErrorCodes, EventNames, MessagesList, MessagesSend, PermissionsGrant, } from "@moltzap/protocol";
5
+ import { AgentsLookup, AgentsLookupByName, agentId as toAgentId, AppsAuthorizeDispatch, bindNotificationHandler, ConversationsCreate, ConversationsGet, conversationId as toConversationId, defineEffectNotificationHandlers, defineNotificationGroup, MessageReceivedNotificationDefinition, ConversationCreatedNotificationDefinition, ConversationUpdatedNotificationDefinition, ConversationArchivedNotificationDefinition, ConversationUnarchivedNotificationDefinition, ErrorCodes, isDecodedNotificationInGroup, messageId as toMessageId, MessagesList, MessagesSend, NotConnectedError, rpcMethods, RpcServerError, RpcTimeoutError, } from "@moltzap/protocol";
6
6
  import { Config, ConfigProvider, Data, Effect, HashMap, Match, Option, Ref, } from "effect";
7
7
  import { MoltZapWsClient, } from "./ws-client.js";
8
- import { AgentNotFoundError, NotConnectedError, RpcServerError, RpcTimeoutError, } from "./runtime/errors.js";
8
+ import { AgentNotFoundError } from "./runtime/errors.js";
9
9
  import { getOr, snapshot } from "./runtime/refs.js";
10
+ import { LocalServiceCommands } from "./runtime/local-service-commands.js";
10
11
  const isPlainRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
11
12
  const CROSS_CONTEXT_TEXT_LIMIT = 120;
12
13
  const DEFAULT_HISTORY_LIMIT = 10;
@@ -74,6 +75,14 @@ const renderPart = Match.type().pipe(Match.discriminatorsExhaustive("type")({
74
75
  * display windows — `conversations get` shows at most a few hundred.
75
76
  */
76
77
  const MAX_MESSAGES_PER_CONV = 1000;
78
+ const serviceNotificationDefinitions = [
79
+ MessageReceivedNotificationDefinition,
80
+ ConversationCreatedNotificationDefinition,
81
+ ConversationUpdatedNotificationDefinition,
82
+ ConversationArchivedNotificationDefinition,
83
+ ConversationUnarchivedNotificationDefinition,
84
+ ];
85
+ const serviceNotificationGroup = defineNotificationGroup("clientService", serviceNotificationDefinitions);
77
86
  /**
78
87
  * Invoke every handler with `arg`, isolating throws so one bad handler
79
88
  * doesn't abort the remaining fanout. Logs via the optional client logger.
@@ -84,7 +93,7 @@ function fanout(handlers, arg, logger) {
84
93
  h(arg);
85
94
  }
86
95
  catch (err) {
87
- logger?.error("event handler threw", err);
96
+ logger?.error("notification handler threw", err);
88
97
  }
89
98
  }
90
99
  }
@@ -110,13 +119,15 @@ export class MoltZapService {
110
119
  lastNotifiedRef = Effect.runSync(Ref.make(HashMap.empty()));
111
120
  lastReadRef = Effect.runSync(Ref.make(HashMap.empty()));
112
121
  archivedConversationIds = new Set();
113
- messageHandlers = [];
114
- rawEventHandlers = [];
115
- disconnectHandlers = [];
116
- reconnectHandlers = [];
117
- permissionRequiredHandlers = [];
118
- conversationArchivedHandlers = [];
119
- conversationUnarchivedHandlers = [];
122
+ handlers = {
123
+ message: [],
124
+ rawNotification: [],
125
+ disconnect: [],
126
+ reconnect: [],
127
+ conversationArchived: [],
128
+ conversationUnarchived: [],
129
+ };
130
+ notificationHandlers = this.createNotificationHandlers();
120
131
  _ownAgentId;
121
132
  constructor(opts) {
122
133
  this.opts = opts;
@@ -137,25 +148,24 @@ export class MoltZapService {
137
148
  // Spec #222 OQ-6: arg required. The body doesn't branch on
138
149
  // close metadata today; signature kept explicit so a future
139
150
  // disconnect-handler chain can plumb code/reason through.
140
- onDisconnect: (_close) => {
141
- void _close;
151
+ onDisconnect: (close) => {
152
+ void close;
142
153
  this._connected = false;
143
- fanout(this.disconnectHandlers, undefined, this.opts.logger);
154
+ fanout(this.handlers.disconnect, undefined, this.opts.logger);
144
155
  },
145
156
  onReconnect: (helloOk) => {
146
157
  this._connected = true;
147
- const hello = helloOk;
148
- this.populateFromHello(hello);
149
- fanout(this.reconnectHandlers, hello, this.opts.logger);
158
+ this.populateFromHello(helloOk);
159
+ fanout(this.handlers.reconnect, helloOk, this.opts.logger);
150
160
  },
151
161
  });
152
- // Spec #222 OQ-4 deletion: per-event `onEvent` callback is gone.
162
+ // Spec #222 OQ-4: per-notification callback is replaced by subscriptions.
153
163
  // Replacement: register a `{}` filter subscription before
154
- // `connect()` so every inbound event still fans out to
155
- // `handleEvent`. Pre-connect registration is supported by the
164
+ // `connect()` so every inbound notification still fans out to
165
+ // `handleNotification`. Pre-connect registration is supported by the
156
166
  // registry.
157
- yield* this.client.subscribe({}, (event) => Effect.sync(() => this.handleEvent(event)));
158
- const helloOk = (yield* this.client.connect());
167
+ yield* this.client.subscribe({}, (notification) => Effect.sync(() => this.handleNotification(notification)));
168
+ const helloOk = yield* this.client.connect();
159
169
  this._connected = true;
160
170
  this._ownAgentId = helloOk.agentId;
161
171
  this.populateFromHello(helloOk);
@@ -331,19 +341,25 @@ export class MoltZapService {
331
341
  handleSocketRequestEffect(method, params) {
332
342
  return Effect.suspend(() => {
333
343
  switch (method) {
334
- case "ping":
344
+ case LocalServiceCommands.Ping:
335
345
  return Effect.succeed({ ok: true, agentId: this.ownAgentId });
336
- case "status":
346
+ case LocalServiceCommands.Status:
337
347
  return Effect.succeed({
338
348
  agentId: this.ownAgentId,
339
349
  connected: this._connected,
340
350
  conversations: this.getConversations().length,
341
351
  });
342
- case "history":
352
+ case LocalServiceCommands.History:
343
353
  return this.handleHistoryRequest(params);
344
- default:
345
- // Passthrough: forward any other method to the MoltZap server via RPC
346
- return this.sendRpc(method, params);
354
+ default: {
355
+ const definition = rpcMethods.find((def) => def.name === method);
356
+ if (definition === undefined || !definition.validateParams(params)) {
357
+ return Effect.fail(new ServiceInputError({
358
+ message: `unknown or invalid RPC method: ${method}`,
359
+ }));
360
+ }
361
+ return this.sendRpc(definition, params);
362
+ }
347
363
  }
348
364
  });
349
365
  }
@@ -360,31 +376,29 @@ export class MoltZapService {
360
376
  const convId = params.conversationId;
361
377
  const limit = params.limit ?? DEFAULT_HISTORY_LIMIT;
362
378
  const sessionKey = params.sessionKey;
363
- const result = (yield* this.sendRpc(MessagesList.name, {
364
- conversationId: convId,
379
+ const result = yield* this.sendRpc(MessagesList, {
380
+ conversationId: toConversationId(convId),
365
381
  limit,
366
- }));
382
+ });
367
383
  // Resolve agent names (batch) + fetch conversation metadata (concurrent)
368
384
  const knownNames = yield* Ref.get(this.agentNamesRef);
369
385
  const unknownAgentIds = [
370
386
  ...new Set(result.messages.map((m) => m.senderId)),
371
387
  ].filter((id) => !HashMap.has(knownNames, id));
372
388
  const lookupEff = unknownAgentIds.length > 0
373
- ? this.sendRpc(AgentsLookup.name, { agentIds: unknownAgentIds }).pipe(Effect.tap((res) => {
374
- const agents = res.agents;
389
+ ? this.sendRpc(AgentsLookup, { agentIds: unknownAgentIds }).pipe(Effect.tap((res) => {
375
390
  return Ref.update(this.agentNamesRef, (names) => {
376
391
  let next = names;
377
- for (const a of agents) {
392
+ for (const a of res.agents) {
378
393
  next = HashMap.set(next, a.id, a.name);
379
394
  }
380
395
  return next;
381
396
  });
382
397
  }), Effect.asVoid, Effect.catchAll(() => Effect.void))
383
398
  : Effect.void;
384
- const metaEff = this.sendRpc(ConversationsGet.name, {
385
- conversationId: convId,
386
- }).pipe(Effect.map((res) => res
387
- .conversation), Effect.catchAll(() => Effect.succeed(undefined)));
399
+ const metaEff = this.sendRpc(ConversationsGet, {
400
+ conversationId: toConversationId(convId),
401
+ }).pipe(Effect.map((res) => res.conversation), Effect.catchAll(() => Effect.succeed(undefined)));
388
402
  const [, convMeta] = yield* Effect.all([lookupEff, metaEff], {
389
403
  concurrency: HISTORY_LOOKUP_CONCURRENCY,
390
404
  });
@@ -460,7 +474,7 @@ export class MoltZapService {
460
474
  const cached = Option.getOrUndefined(HashMap.get(snapshot(this.agentNamesRef), agentId));
461
475
  if (cached !== undefined)
462
476
  return cached;
463
- return yield* this.sendRpc(AgentsLookup.name, {
477
+ return yield* this.sendRpc(AgentsLookup, {
464
478
  agentIds: [agentId],
465
479
  }).pipe(Effect.flatMap((result) => {
466
480
  const agent = result.agents[0];
@@ -478,33 +492,43 @@ export class MoltZapService {
478
492
  message: "Conversation is archived",
479
493
  }));
480
494
  }
481
- return Effect.asVoid(this.sendRpc(MessagesSend.name, {
482
- conversationId: convId,
495
+ return Effect.asVoid(this.sendRpc(MessagesSend, {
496
+ conversationId: toConversationId(convId),
483
497
  parts: [{ type: "text", text }],
484
- ...(opts?.replyTo ? { replyToId: opts.replyTo } : {}),
498
+ ...(opts?.replyTo ? { replyToId: toMessageId(opts.replyTo) } : {}),
485
499
  ...(opts?.dispatchLeaseId
486
500
  ? { dispatchLeaseId: opts.dispatchLeaseId }
487
501
  : {}),
488
502
  }));
489
503
  }
490
504
  authorizeDispatch(request) {
491
- return this.sendRpc(AppsAuthorizeDispatch.name, {
492
- conversationId: request.conversationId,
493
- messageId: request.message.id,
494
- senderAgentId: request.senderAgentId,
495
- parts: request.message.parts,
505
+ return this.sendRpc(AppsAuthorizeDispatch, {
506
+ conversationId: toConversationId(request.conversationId),
507
+ messageId: toMessageId(request.message.id),
508
+ senderAgentId: toAgentId(request.senderAgentId),
509
+ parts: [...request.message.parts],
496
510
  receivedAt: request.receivedAt,
497
511
  clock: request.clock,
498
- pending: request.pending,
512
+ pending: request.pending.map((pending) => ({
513
+ ...pending,
514
+ messageId: toMessageId(pending.messageId),
515
+ conversationId: toConversationId(pending.conversationId),
516
+ senderAgentId: toAgentId(pending.senderAgentId),
517
+ ...(pending.parts !== undefined ? { parts: [...pending.parts] } : {}),
518
+ })),
499
519
  attempt: request.attempt,
500
520
  }, { timeoutMs: DISPATCH_AUTHORIZATION_RPC_TIMEOUT_MS }).pipe(Effect.map((result) => {
501
- const admission = result.admission;
521
+ const { admission } = result;
502
522
  switch (admission.decision) {
503
523
  case "grant":
504
524
  return {
505
525
  _tag: "grant",
506
- leaseId: admission.leaseId,
507
- leaseTimeoutMs: admission.leaseTimeoutMs,
526
+ ...(admission.leaseId !== undefined
527
+ ? { leaseId: admission.leaseId }
528
+ : {}),
529
+ ...(admission.leaseTimeoutMs !== undefined
530
+ ? { leaseTimeoutMs: admission.leaseTimeoutMs }
531
+ : {}),
508
532
  ...(admission.dispatchMessageId
509
533
  ? { dispatchMessageId: admission.dispatchMessageId }
510
534
  : {}),
@@ -512,12 +536,16 @@ export class MoltZapService {
512
536
  case "deny":
513
537
  return {
514
538
  _tag: "deny",
515
- reason: admission.reason,
539
+ ...(admission.reason !== undefined
540
+ ? { reason: admission.reason }
541
+ : {}),
516
542
  };
517
543
  case "hold":
518
544
  return {
519
545
  _tag: "hold",
520
- reason: admission.reason,
546
+ ...(admission.reason !== undefined
547
+ ? { reason: admission.reason }
548
+ : {}),
521
549
  };
522
550
  }
523
551
  }));
@@ -527,17 +555,17 @@ export class MoltZapService {
527
555
  const cache = yield* Ref.get(this.agentConversationCacheRef);
528
556
  let conversationId = Option.getOrUndefined(HashMap.get(cache, agentName));
529
557
  if (!conversationId) {
530
- const lookupResult = (yield* this.sendRpc(AgentsLookupByName.name, {
558
+ const lookupResult = yield* this.sendRpc(AgentsLookupByName, {
531
559
  names: [agentName],
532
- }));
560
+ });
533
561
  const agent = lookupResult.agents[0];
534
562
  if (!agent) {
535
563
  return yield* Effect.fail(new AgentNotFoundError({ agentName }));
536
564
  }
537
- const createResult = (yield* this.sendRpc(ConversationsCreate.name, {
565
+ const createResult = yield* this.sendRpc(ConversationsCreate, {
538
566
  type: "dm",
539
- participants: [{ type: "agent", id: agent.id }],
540
- }));
567
+ participants: [{ type: "agent", id: toAgentId(agent.id) }],
568
+ });
541
569
  conversationId = createResult.conversation.id;
542
570
  const newId = conversationId;
543
571
  yield* Ref.update(this.agentConversationCacheRef, (m) => HashMap.set(m, agentName, newId));
@@ -671,57 +699,37 @@ export class MoltZapService {
671
699
  return HashMap.set(outer, currentConvId, markers);
672
700
  }));
673
701
  }
674
- on(event, handler) {
675
- switch (event) {
676
- case "message":
677
- this.messageHandlers.push(handler);
678
- break;
679
- case "rawEvent":
680
- this.rawEventHandlers.push(handler);
681
- break;
682
- case "disconnect":
683
- this.disconnectHandlers.push(handler);
684
- break;
685
- case "reconnect":
686
- this.reconnectHandlers.push(handler);
687
- break;
688
- case "permissionRequired":
689
- this.permissionRequiredHandlers.push(handler);
690
- break;
691
- case "conversationArchived":
692
- this.conversationArchivedHandlers.push(handler);
693
- break;
694
- case "conversationUnarchived":
695
- this.conversationUnarchivedHandlers.push(handler);
696
- break;
697
- }
702
+ // --- Notifications ---
703
+ on(notification, handler) {
704
+ this.handlers[notification].push(handler);
698
705
  }
699
706
  // --- RPC passthrough ---
700
- sendRpc(method, params, opts) {
707
+ sendRpc(definition, params, opts) {
701
708
  return Effect.suspend(() => {
702
709
  if (!this.client) {
703
710
  return Effect.fail(new NotConnectedError({ message: "Not connected" }));
704
711
  }
705
- return this.client.sendRpc(method, params, opts);
712
+ return this.client.sendRpc(definition, params, opts);
706
713
  });
707
714
  }
708
- grantPermission(params) {
709
- return Effect.asVoid(this.sendRpc(PermissionsGrant.name, params));
710
- }
711
715
  // --- Internals ---
712
716
  /**
713
717
  * Fetch full conversation details (including participants) via RPC and merge
714
- * into the local cache. Called on ConversationCreated events because the
715
- * event schema omits the participants list — see protocol/events.ts.
718
+ * into the local cache. Called on ConversationCreated notifications because
719
+ * the notification schema omits the participants list — see
720
+ * protocol/notifications.ts.
716
721
  */
717
722
  refreshConversationParticipants(conversationId) {
718
- return this.sendRpc(ConversationsGet.name, { conversationId }).pipe(Effect.tap((res) => {
719
- const typed = res;
723
+ return this.sendRpc(ConversationsGet, {
724
+ conversationId: toConversationId(conversationId),
725
+ }).pipe(Effect.tap((res) => {
720
726
  const meta = {
721
- id: typed.conversation.id,
722
- type: typed.conversation.type,
723
- name: typed.conversation.name,
724
- participants: typed.participants.map((p) => `${p.participant.type}:${p.participant.id}`),
727
+ id: res.conversation.id,
728
+ type: res.conversation.type,
729
+ participants: res.participants.map((p) => `${p.participant.type}:${p.participant.id}`),
730
+ ...(res.conversation.name !== undefined
731
+ ? { name: res.conversation.name }
732
+ : {}),
725
733
  };
726
734
  if (this.archivedConversationIds.has(conversationId)) {
727
735
  return Effect.void;
@@ -742,92 +750,88 @@ export class MoltZapService {
742
750
  next = HashMap.set(next, conv.id, {
743
751
  id: conv.id,
744
752
  type: conv.type,
745
- name: conv.name,
746
753
  participants: (conv.participants ?? []).map((p) => `${p.type}:${p.id}`),
754
+ ...(conv.name !== undefined ? { name: conv.name } : {}),
747
755
  });
748
756
  }
749
757
  return next;
750
758
  }));
751
759
  }
752
- handleEvent(event) {
753
- const eventData = isPlainRecord(event.data) ? event.data : {};
754
- const message = isPlainRecord(eventData["message"])
755
- ? eventData["message"]
760
+ handleNotification(notification) {
761
+ const params = isPlainRecord(notification.params)
762
+ ? notification.params
763
+ : {};
764
+ const message = isPlainRecord(params["message"])
765
+ ? params["message"]
756
766
  : undefined;
757
- const conversation = isPlainRecord(eventData["conversation"])
758
- ? eventData["conversation"]
767
+ const conversation = isPlainRecord(params["conversation"])
768
+ ? params["conversation"]
759
769
  : undefined;
760
- const eventConversationId = typeof eventData["conversationId"] === "string"
761
- ? eventData["conversationId"]
770
+ const notificationConversationId = typeof params["conversationId"] === "string"
771
+ ? params["conversationId"]
762
772
  : undefined;
763
773
  appendClientEventTrace({
764
774
  ts: new Date().toISOString(),
765
775
  agentId: this._ownAgentId ?? "unknown",
766
- event: event.event,
776
+ notification: notification.method,
767
777
  messageId: message?.["id"],
768
778
  messageConversationId: message?.["conversationId"],
769
779
  messageSenderId: message?.["senderId"],
770
- conversationId: conversation?.["id"] ?? eventConversationId,
780
+ conversationId: conversation?.["id"] ?? notificationConversationId,
771
781
  conversationName: conversation?.["name"],
772
782
  });
773
- fanout(this.rawEventHandlers, event, this.opts.logger);
774
- // The server validates event.data against each event's schema before
775
- // emitting; each case casts to the typed Static<> payload for that
776
- // specific event.
777
- switch (event.event) {
778
- case EventNames.MessageReceived: {
779
- const msg = event.data.message;
780
- this.storeMessage(msg);
781
- // Name resolution is driven lazily by channel-core's serialized
782
- // consumer via resolveAgentName(), which populates agentNamesRef on
783
- // first miss and hits the cache on every subsequent message.
784
- if (msg.senderId !== this._ownAgentId) {
785
- fanout(this.messageHandlers, msg, this.opts.logger);
786
- }
787
- break;
788
- }
789
- case EventNames.PermissionsRequired: {
790
- fanout(this.permissionRequiredHandlers, event.data, this.opts.logger);
791
- break;
792
- }
793
- case EventNames.ConversationCreated:
794
- case EventNames.ConversationUpdated: {
795
- const { conversation } = event.data;
796
- this.archivedConversationIds.delete(conversation.id);
797
- Effect.runSync(Ref.update(this.conversationsRef, (m) => {
798
- const existing = Option.getOrUndefined(HashMap.get(m, conversation.id));
799
- return HashMap.set(m, conversation.id, {
800
- id: conversation.id,
801
- type: conversation.type,
802
- name: conversation.name,
803
- participants: existing?.participants ?? [],
804
- });
805
- }));
806
- // The ConversationCreated event schema doesn't carry participants
807
- // (protocol/schema/events.ts: ConversationSchema is id/type/name/
808
- // createdBy/timestamps only). Fetch full details asynchronously so
809
- // downstream code that reads getConversation(id).participants sees
810
- // a populated list within a round-trip of the event.
811
- if (event.event === EventNames.ConversationCreated) {
812
- // Fire-and-forget: refreshConversationParticipants never fails.
813
- Effect.runFork(this.refreshConversationParticipants(conversation.id));
814
- }
815
- break;
816
- }
817
- case EventNames.ConversationArchived: {
818
- const data = event.data;
819
- this.markConversationArchived(data.conversationId);
820
- fanout(this.conversationArchivedHandlers, data, this.opts.logger);
821
- break;
822
- }
823
- case EventNames.ConversationUnarchived: {
824
- const data = event.data;
825
- this.archivedConversationIds.delete(data.conversationId);
826
- fanout(this.conversationUnarchivedHandlers, data, this.opts.logger);
827
- break;
828
- }
783
+ fanout(this.handlers.rawNotification, notification, this.opts.logger);
784
+ if (isDecodedNotificationInGroup(serviceNotificationGroup, notification)) {
785
+ Effect.runSync(this.notificationHandlers.dispatch(notification));
829
786
  }
830
787
  }
788
+ createNotificationHandlers() {
789
+ return defineEffectNotificationHandlers(serviceNotificationGroup, [
790
+ bindNotificationHandler(MessageReceivedNotificationDefinition, (params) => Effect.sync(() => this.handleMessageReceivedNotification(params))),
791
+ bindNotificationHandler(ConversationCreatedNotificationDefinition, (params) => Effect.sync(() => this.handleConversationCreatedNotification(params))),
792
+ bindNotificationHandler(ConversationUpdatedNotificationDefinition, (params) => Effect.sync(() => this.handleConversationUpdatedNotification(params))),
793
+ bindNotificationHandler(ConversationArchivedNotificationDefinition, (params) => Effect.sync(() => {
794
+ this.markConversationArchived(params.conversationId);
795
+ fanout(this.handlers.conversationArchived, params, this.opts.logger);
796
+ })),
797
+ bindNotificationHandler(ConversationUnarchivedNotificationDefinition, (params) => Effect.sync(() => {
798
+ this.archivedConversationIds.delete(params.conversationId);
799
+ fanout(this.handlers.conversationUnarchived, params, this.opts.logger);
800
+ })),
801
+ ]);
802
+ }
803
+ handleMessageReceivedNotification(notification) {
804
+ const msg = notification.message;
805
+ this.storeMessage(msg);
806
+ // Name resolution is driven lazily by channel-core's serialized consumer
807
+ // via resolveAgentName(), which populates agentNamesRef on first miss and
808
+ // hits the cache on every subsequent message.
809
+ if (msg.senderId !== this._ownAgentId) {
810
+ fanout(this.handlers.message, msg, this.opts.logger);
811
+ }
812
+ }
813
+ handleConversationCreatedNotification(notification) {
814
+ this.upsertConversationNotification(notification);
815
+ Effect.runFork(this.refreshConversationParticipants(notification.conversation.id));
816
+ }
817
+ handleConversationUpdatedNotification(notification) {
818
+ this.upsertConversationNotification(notification);
819
+ }
820
+ upsertConversationNotification(notification) {
821
+ const { conversation } = notification;
822
+ this.archivedConversationIds.delete(conversation.id);
823
+ Effect.runSync(Ref.update(this.conversationsRef, (m) => {
824
+ const existing = Option.getOrUndefined(HashMap.get(m, conversation.id));
825
+ return HashMap.set(m, conversation.id, {
826
+ id: conversation.id,
827
+ type: conversation.type,
828
+ participants: existing?.participants ?? [],
829
+ ...(conversation.name !== undefined
830
+ ? { name: conversation.name }
831
+ : {}),
832
+ });
833
+ }));
834
+ }
831
835
  markConversationArchived(conversationId) {
832
836
  this.archivedConversationIds.add(conversationId);
833
837
  Effect.runSync(Effect.all([