@moltzap/client 2026.503.3 → 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 (227) hide show
  1. package/dist/auth.d.ts +10 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +8 -5
  4. package/dist/auth.js.map +1 -1
  5. package/dist/channel-core.d.ts +1 -4
  6. package/dist/channel-core.d.ts.map +1 -1
  7. package/dist/channel-core.js +0 -9
  8. package/dist/channel-core.js.map +1 -1
  9. package/dist/channel-core.test.js +90 -71
  10. package/dist/channel-core.test.js.map +1 -1
  11. package/dist/cli/commands/agents.d.ts.map +1 -1
  12. package/dist/cli/commands/agents.js +5 -3
  13. package/dist/cli/commands/agents.js.map +1 -1
  14. package/dist/cli/commands/apps.d.ts +9 -27
  15. package/dist/cli/commands/apps.d.ts.map +1 -1
  16. package/dist/cli/commands/apps.js +36 -57
  17. package/dist/cli/commands/apps.js.map +1 -1
  18. package/dist/cli/commands/apps.test.js +63 -75
  19. package/dist/cli/commands/apps.test.js.map +1 -1
  20. package/dist/cli/commands/contacts.d.ts +1 -5
  21. package/dist/cli/commands/contacts.d.ts.map +1 -1
  22. package/dist/cli/commands/contacts.js +14 -27
  23. package/dist/cli/commands/contacts.js.map +1 -1
  24. package/dist/cli/commands/conversations-v2.test.js +34 -29
  25. package/dist/cli/commands/conversations-v2.test.js.map +1 -1
  26. package/dist/cli/commands/conversations.d.ts +7 -3
  27. package/dist/cli/commands/conversations.d.ts.map +1 -1
  28. package/dist/cli/commands/conversations.js +63 -48
  29. package/dist/cli/commands/conversations.js.map +1 -1
  30. package/dist/cli/commands/messages.d.ts +7 -3
  31. package/dist/cli/commands/messages.d.ts.map +1 -1
  32. package/dist/cli/commands/messages.js +13 -15
  33. package/dist/cli/commands/messages.js.map +1 -1
  34. package/dist/cli/commands/messages.test.js +30 -31
  35. package/dist/cli/commands/messages.test.js.map +1 -1
  36. package/dist/cli/commands/ping.d.ts +9 -0
  37. package/dist/cli/commands/ping.d.ts.map +1 -1
  38. package/dist/cli/commands/ping.js +17 -4
  39. package/dist/cli/commands/ping.js.map +1 -1
  40. package/dist/cli/commands/presence.d.ts.map +1 -1
  41. package/dist/cli/commands/presence.js +6 -4
  42. package/dist/cli/commands/presence.js.map +1 -1
  43. package/dist/cli/commands/register.d.ts.map +1 -1
  44. package/dist/cli/commands/register.js +2 -1
  45. package/dist/cli/commands/register.js.map +1 -1
  46. package/dist/cli/commands/send.d.ts.map +1 -1
  47. package/dist/cli/commands/send.js +22 -11
  48. package/dist/cli/commands/send.js.map +1 -1
  49. package/dist/cli/commands/send.test.js +12 -9
  50. package/dist/cli/commands/send.test.js.map +1 -1
  51. package/dist/cli/commands/status.js +2 -2
  52. package/dist/cli/commands/status.js.map +1 -1
  53. package/dist/cli/commands/test-transport.d.ts +10 -0
  54. package/dist/cli/commands/test-transport.d.ts.map +1 -0
  55. package/dist/cli/commands/test-transport.js +29 -0
  56. package/dist/cli/commands/test-transport.js.map +1 -0
  57. package/dist/cli/commands/whoami.d.ts.map +1 -1
  58. package/dist/cli/commands/whoami.js +5 -1
  59. package/dist/cli/commands/whoami.js.map +1 -1
  60. package/dist/cli/config.d.ts +42 -6
  61. package/dist/cli/config.d.ts.map +1 -1
  62. package/dist/cli/config.js +91 -27
  63. package/dist/cli/config.js.map +1 -1
  64. package/dist/cli/http-client.d.ts +11 -1
  65. package/dist/cli/http-client.d.ts.map +1 -1
  66. package/dist/cli/http-client.js +13 -4
  67. package/dist/cli/http-client.js.map +1 -1
  68. package/dist/cli/index.d.ts.map +1 -1
  69. package/dist/cli/index.js +2 -3
  70. package/dist/cli/index.js.map +1 -1
  71. package/dist/cli/profile.d.ts +3 -4
  72. package/dist/cli/profile.d.ts.map +1 -1
  73. package/dist/cli/profile.js +42 -29
  74. package/dist/cli/profile.js.map +1 -1
  75. package/dist/cli/runtime.d.ts.map +1 -1
  76. package/dist/cli/runtime.js +9 -4
  77. package/dist/cli/runtime.js.map +1 -1
  78. package/dist/cli/socket-client.d.ts +29 -4
  79. package/dist/cli/socket-client.d.ts.map +1 -1
  80. package/dist/cli/socket-client.js +52 -21
  81. package/dist/cli/socket-client.js.map +1 -1
  82. package/dist/cli/transport.d.ts +6 -9
  83. package/dist/cli/transport.d.ts.map +1 -1
  84. package/dist/cli/transport.js +47 -37
  85. package/dist/cli/transport.js.map +1 -1
  86. package/dist/cli/transport.test.js +19 -16
  87. package/dist/cli/transport.test.js.map +1 -1
  88. package/dist/index.d.ts +2 -3
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +1 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/internal/__tests__/app-callback-partition-key.test.d.ts +2 -0
  93. package/dist/internal/__tests__/app-callback-partition-key.test.d.ts.map +1 -0
  94. package/dist/internal/__tests__/app-callback-partition-key.test.js +33 -0
  95. package/dist/internal/__tests__/app-callback-partition-key.test.js.map +1 -0
  96. package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts +2 -0
  97. package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts.map +1 -0
  98. package/dist/internal/__tests__/{s2c-partition-worker.test.js → app-callback-partition-worker.test.js} +6 -8
  99. package/dist/internal/__tests__/app-callback-partition-worker.test.js.map +1 -0
  100. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts +2 -0
  101. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts.map +1 -0
  102. package/dist/internal/__tests__/{s2c-partitioned-dispatcher-real-ws.test.js → app-callback-partitioned-dispatcher-real-ws.test.js} +69 -93
  103. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.js.map +1 -0
  104. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts +2 -0
  105. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts.map +1 -0
  106. package/dist/internal/__tests__/{s2c-partitioned-dispatcher.test.js → app-callback-partitioned-dispatcher.test.js} +36 -69
  107. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.js.map +1 -0
  108. package/dist/internal/__tests__/app-callback-test-requests.d.ts +58 -0
  109. package/dist/internal/__tests__/app-callback-test-requests.d.ts.map +1 -0
  110. package/dist/internal/__tests__/app-callback-test-requests.js +124 -0
  111. package/dist/internal/__tests__/app-callback-test-requests.js.map +1 -0
  112. package/dist/internal/{s2c-dispatcher-errors.d.ts → app-callback-dispatcher-errors.d.ts} +5 -21
  113. package/dist/internal/app-callback-dispatcher-errors.d.ts.map +1 -0
  114. package/dist/internal/{s2c-dispatcher-errors.js → app-callback-dispatcher-errors.js} +3 -13
  115. package/dist/internal/app-callback-dispatcher-errors.js.map +1 -0
  116. package/dist/internal/app-callback-partition-key.d.ts +68 -0
  117. package/dist/internal/app-callback-partition-key.d.ts.map +1 -0
  118. package/dist/internal/app-callback-partition-key.js +90 -0
  119. package/dist/internal/app-callback-partition-key.js.map +1 -0
  120. package/dist/internal/{s2c-partition-worker.d.ts → app-callback-partition-worker.d.ts} +5 -5
  121. package/dist/internal/app-callback-partition-worker.d.ts.map +1 -0
  122. package/dist/internal/{s2c-partition-worker.js → app-callback-partition-worker.js} +5 -5
  123. package/dist/internal/app-callback-partition-worker.js.map +1 -0
  124. package/dist/internal/{s2c-partitioned-dispatcher.d.ts → app-callback-partitioned-dispatcher.d.ts} +9 -10
  125. package/dist/internal/app-callback-partitioned-dispatcher.d.ts.map +1 -0
  126. package/dist/internal/{s2c-partitioned-dispatcher.js → app-callback-partitioned-dispatcher.js} +13 -21
  127. package/dist/internal/app-callback-partitioned-dispatcher.js.map +1 -0
  128. package/dist/runtime/close-info.d.ts +15 -16
  129. package/dist/runtime/close-info.d.ts.map +1 -1
  130. package/dist/runtime/close-info.js +12 -8
  131. package/dist/runtime/close-info.js.map +1 -1
  132. package/dist/runtime/errors.d.ts +2 -28
  133. package/dist/runtime/errors.d.ts.map +1 -1
  134. package/dist/runtime/errors.js +0 -9
  135. package/dist/runtime/errors.js.map +1 -1
  136. package/dist/runtime/errors.test.js +4 -3
  137. package/dist/runtime/errors.test.js.map +1 -1
  138. package/dist/runtime/frame.d.ts +17 -18
  139. package/dist/runtime/frame.d.ts.map +1 -1
  140. package/dist/runtime/frame.js +86 -31
  141. package/dist/runtime/frame.js.map +1 -1
  142. package/dist/runtime/frame.test.js +15 -11
  143. package/dist/runtime/frame.test.js.map +1 -1
  144. package/dist/runtime/index.d.ts +1 -1
  145. package/dist/runtime/index.d.ts.map +1 -1
  146. package/dist/runtime/index.js +1 -1
  147. package/dist/runtime/index.js.map +1 -1
  148. package/dist/runtime/local-service-commands.d.ts +7 -0
  149. package/dist/runtime/local-service-commands.d.ts.map +1 -0
  150. package/dist/runtime/local-service-commands.js +6 -0
  151. package/dist/runtime/local-service-commands.js.map +1 -0
  152. package/dist/runtime/runtime.test.js +3 -3
  153. package/dist/runtime/runtime.test.js.map +1 -1
  154. package/dist/runtime/subscribers.d.ts +37 -33
  155. package/dist/runtime/subscribers.d.ts.map +1 -1
  156. package/dist/runtime/subscribers.js +22 -21
  157. package/dist/runtime/subscribers.js.map +1 -1
  158. package/dist/runtime/subscribers.test.js +50 -31
  159. package/dist/runtime/subscribers.test.js.map +1 -1
  160. package/dist/service.d.ts +24 -39
  161. package/dist/service.d.ts.map +1 -1
  162. package/dist/service.js +190 -171
  163. package/dist/service.js.map +1 -1
  164. package/dist/service.test.js +117 -175
  165. package/dist/service.test.js.map +1 -1
  166. package/dist/test/index.d.ts +9 -1
  167. package/dist/test/index.d.ts.map +1 -1
  168. package/dist/test/index.js +14 -3
  169. package/dist/test/index.js.map +1 -1
  170. package/dist/test-utils/channel-service-fixture.d.ts +1 -2
  171. package/dist/test-utils/channel-service-fixture.d.ts.map +1 -1
  172. package/dist/test-utils/channel-service-fixture.js +28 -20
  173. package/dist/test-utils/channel-service-fixture.js.map +1 -1
  174. package/dist/test-utils/conformance-adapter.d.ts +2 -2
  175. package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
  176. package/dist/test-utils/conformance-adapter.js +78 -54
  177. package/dist/test-utils/conformance-adapter.js.map +1 -1
  178. package/dist/test-utils/fake-service.d.ts +14 -39
  179. package/dist/test-utils/fake-service.d.ts.map +1 -1
  180. package/dist/test-utils/fake-service.js +33 -39
  181. package/dist/test-utils/fake-service.js.map +1 -1
  182. package/dist/test-utils/ids.d.ts +9 -0
  183. package/dist/test-utils/ids.d.ts.map +1 -0
  184. package/dist/test-utils/ids.js +52 -0
  185. package/dist/test-utils/ids.js.map +1 -0
  186. package/dist/test-utils/index.d.ts +10 -2
  187. package/dist/test-utils/index.d.ts.map +1 -1
  188. package/dist/test-utils/index.js +25 -9
  189. package/dist/test-utils/index.js.map +1 -1
  190. package/dist/ws-client.d.ts +59 -64
  191. package/dist/ws-client.d.ts.map +1 -1
  192. package/dist/ws-client.js +228 -175
  193. package/dist/ws-client.js.map +1 -1
  194. package/dist/ws-client.test.js +190 -176
  195. package/dist/ws-client.test.js.map +1 -1
  196. package/package.json +3 -3
  197. package/dist/cli/commands/permissions.d.ts +0 -66
  198. package/dist/cli/commands/permissions.d.ts.map +0 -1
  199. package/dist/cli/commands/permissions.js +0 -101
  200. package/dist/cli/commands/permissions.js.map +0 -1
  201. package/dist/cli/commands/permissions.test.d.ts +0 -2
  202. package/dist/cli/commands/permissions.test.d.ts.map +0 -1
  203. package/dist/cli/commands/permissions.test.js +0 -126
  204. package/dist/cli/commands/permissions.test.js.map +0 -1
  205. package/dist/internal/__tests__/s2c-partition-key.test.d.ts +0 -2
  206. package/dist/internal/__tests__/s2c-partition-key.test.d.ts.map +0 -1
  207. package/dist/internal/__tests__/s2c-partition-key.test.js +0 -230
  208. package/dist/internal/__tests__/s2c-partition-key.test.js.map +0 -1
  209. package/dist/internal/__tests__/s2c-partition-worker.test.d.ts +0 -2
  210. package/dist/internal/__tests__/s2c-partition-worker.test.d.ts.map +0 -1
  211. package/dist/internal/__tests__/s2c-partition-worker.test.js.map +0 -1
  212. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts +0 -2
  213. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts.map +0 -1
  214. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.js.map +0 -1
  215. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts +0 -2
  216. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts.map +0 -1
  217. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.js.map +0 -1
  218. package/dist/internal/s2c-dispatcher-errors.d.ts.map +0 -1
  219. package/dist/internal/s2c-dispatcher-errors.js.map +0 -1
  220. package/dist/internal/s2c-partition-key.d.ts +0 -71
  221. package/dist/internal/s2c-partition-key.d.ts.map +0 -1
  222. package/dist/internal/s2c-partition-key.js +0 -140
  223. package/dist/internal/s2c-partition-key.js.map +0 -1
  224. package/dist/internal/s2c-partition-worker.d.ts.map +0 -1
  225. package/dist/internal/s2c-partition-worker.js.map +0 -1
  226. package/dist/internal/s2c-partitioned-dispatcher.d.ts.map +0 -1
  227. package/dist/internal/s2c-partitioned-dispatcher.js.map +0 -1
package/dist/service.js CHANGED
@@ -2,14 +2,26 @@ 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";
6
- import { Effect, HashMap, Match, Option, Ref } from "effect";
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
+ 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);
12
+ const CROSS_CONTEXT_TEXT_LIMIT = 120;
13
+ const DEFAULT_HISTORY_LIMIT = 10;
14
+ const DEFAULT_MAX_CONTEXT_CONVERSATIONS = 5;
15
+ const DEFAULT_MAX_MESSAGES_PER_CONVERSATION = 3;
16
+ const HISTORY_LOOKUP_CONCURRENCY = 2;
17
+ const MILLISECONDS_PER_MINUTE = 60_000;
18
+ const SOCKET_FILE_MODE = 0o600;
19
+ class ServiceInputError extends Data.TaggedError("ServiceInputError") {
20
+ }
21
+ const ClientEventLogDir = Config.option(Config.string("MOLTZAP_CLIENT_EVENT_LOG_DIR"));
22
+ const getClientEventLogDir = () => Option.getOrUndefined(Effect.runSync(ClientEventLogDir.pipe(Effect.withConfigProvider(ConfigProvider.fromEnv()))));
11
23
  function appendClientEventTrace(record) {
12
- const dir = process.env["MOLTZAP_CLIENT_EVENT_LOG_DIR"];
24
+ const dir = getClientEventLogDir();
13
25
  if (!dir)
14
26
  return;
15
27
  try {
@@ -41,7 +53,7 @@ export function formatCrossConversationBlock(entries, opts) {
41
53
  return null;
42
54
  const lines = entries.map((e) => {
43
55
  const safeSender = sanitizeForSystemReminder(e.senderName);
44
- const safeText = sanitizeForSystemReminder(e.text.slice(0, 120));
56
+ const safeText = sanitizeForSystemReminder(e.text.slice(0, CROSS_CONTEXT_TEXT_LIMIT));
45
57
  return `@${safeSender} (${e.minutesAgo}m ago): (${e.count} new) "${safeText}"`;
46
58
  });
47
59
  return [
@@ -63,6 +75,14 @@ const renderPart = Match.type().pipe(Match.discriminatorsExhaustive("type")({
63
75
  * display windows — `conversations get` shows at most a few hundred.
64
76
  */
65
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);
66
86
  /**
67
87
  * Invoke every handler with `arg`, isolating throws so one bad handler
68
88
  * doesn't abort the remaining fanout. Logs via the optional client logger.
@@ -73,7 +93,7 @@ function fanout(handlers, arg, logger) {
73
93
  h(arg);
74
94
  }
75
95
  catch (err) {
76
- logger?.error("event handler threw", err);
96
+ logger?.error("notification handler threw", err);
77
97
  }
78
98
  }
79
99
  }
@@ -99,13 +119,15 @@ export class MoltZapService {
99
119
  lastNotifiedRef = Effect.runSync(Ref.make(HashMap.empty()));
100
120
  lastReadRef = Effect.runSync(Ref.make(HashMap.empty()));
101
121
  archivedConversationIds = new Set();
102
- messageHandlers = [];
103
- rawEventHandlers = [];
104
- disconnectHandlers = [];
105
- reconnectHandlers = [];
106
- permissionRequiredHandlers = [];
107
- conversationArchivedHandlers = [];
108
- conversationUnarchivedHandlers = [];
122
+ handlers = {
123
+ message: [],
124
+ rawNotification: [],
125
+ disconnect: [],
126
+ reconnect: [],
127
+ conversationArchived: [],
128
+ conversationUnarchived: [],
129
+ };
130
+ notificationHandlers = this.createNotificationHandlers();
109
131
  _ownAgentId;
110
132
  constructor(opts) {
111
133
  this.opts = opts;
@@ -126,24 +148,24 @@ export class MoltZapService {
126
148
  // Spec #222 OQ-6: arg required. The body doesn't branch on
127
149
  // close metadata today; signature kept explicit so a future
128
150
  // disconnect-handler chain can plumb code/reason through.
129
- onDisconnect: (_close) => {
151
+ onDisconnect: (close) => {
152
+ void close;
130
153
  this._connected = false;
131
- fanout(this.disconnectHandlers, undefined, this.opts.logger);
154
+ fanout(this.handlers.disconnect, undefined, this.opts.logger);
132
155
  },
133
156
  onReconnect: (helloOk) => {
134
157
  this._connected = true;
135
- const hello = helloOk;
136
- this.populateFromHello(hello);
137
- fanout(this.reconnectHandlers, hello, this.opts.logger);
158
+ this.populateFromHello(helloOk);
159
+ fanout(this.handlers.reconnect, helloOk, this.opts.logger);
138
160
  },
139
161
  });
140
- // Spec #222 OQ-4 deletion: per-event `onEvent` callback is gone.
162
+ // Spec #222 OQ-4: per-notification callback is replaced by subscriptions.
141
163
  // Replacement: register a `{}` filter subscription before
142
- // `connect()` so every inbound event still fans out to
143
- // `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
144
166
  // registry.
145
- yield* this.client.subscribe({}, (event) => Effect.sync(() => this.handleEvent(event)));
146
- const helloOk = (yield* this.client.connect());
167
+ yield* this.client.subscribe({}, (notification) => Effect.sync(() => this.handleNotification(notification)));
168
+ const helloOk = yield* this.client.connect();
147
169
  this._connected = true;
148
170
  this._ownAgentId = helloOk.agentId;
149
171
  this.populateFromHello(helloOk);
@@ -232,7 +254,7 @@ export class MoltZapService {
232
254
  // impersonates this agent to the server, so other local users on the
233
255
  // host must not be able to connect.
234
256
  try {
235
- fs.chmodSync(sockPath, 0o600);
257
+ fs.chmodSync(sockPath, SOCKET_FILE_MODE);
236
258
  }
237
259
  catch (err) {
238
260
  this.opts.logger?.warn("chmod 0600 on socket failed", err);
@@ -319,60 +341,66 @@ export class MoltZapService {
319
341
  handleSocketRequestEffect(method, params) {
320
342
  return Effect.suspend(() => {
321
343
  switch (method) {
322
- case "ping":
344
+ case LocalServiceCommands.Ping:
323
345
  return Effect.succeed({ ok: true, agentId: this.ownAgentId });
324
- case "status":
346
+ case LocalServiceCommands.Status:
325
347
  return Effect.succeed({
326
348
  agentId: this.ownAgentId,
327
349
  connected: this._connected,
328
350
  conversations: this.getConversations().length,
329
351
  });
330
- case "history":
352
+ case LocalServiceCommands.History:
331
353
  return this.handleHistoryRequest(params);
332
- default:
333
- // Passthrough: forward any other method to the MoltZap server via RPC
334
- 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
+ }
335
363
  }
336
364
  });
337
365
  }
338
366
  handleHistoryRequest(params) {
339
367
  return Effect.gen(this, function* () {
340
368
  if (typeof params.conversationId !== "string" || !params.conversationId) {
341
- return yield* Effect.fail(new Error("conversationId is required and must be a string"));
369
+ return yield* Effect.fail(new ServiceInputError({
370
+ message: "conversationId is required and must be a string",
371
+ }));
342
372
  }
343
373
  if (params.limit !== undefined && typeof params.limit !== "number") {
344
- return yield* Effect.fail(new Error("limit must be a number"));
374
+ return yield* Effect.fail(new ServiceInputError({ message: "limit must be a number" }));
345
375
  }
346
376
  const convId = params.conversationId;
347
- const limit = params.limit ?? 10;
377
+ const limit = params.limit ?? DEFAULT_HISTORY_LIMIT;
348
378
  const sessionKey = params.sessionKey;
349
- const result = (yield* this.sendRpc(MessagesList.name, {
350
- conversationId: convId,
379
+ const result = yield* this.sendRpc(MessagesList, {
380
+ conversationId: toConversationId(convId),
351
381
  limit,
352
- }));
382
+ });
353
383
  // Resolve agent names (batch) + fetch conversation metadata (concurrent)
354
384
  const knownNames = yield* Ref.get(this.agentNamesRef);
355
385
  const unknownAgentIds = [
356
386
  ...new Set(result.messages.map((m) => m.senderId)),
357
387
  ].filter((id) => !HashMap.has(knownNames, id));
358
388
  const lookupEff = unknownAgentIds.length > 0
359
- ? this.sendRpc(AgentsLookup.name, { agentIds: unknownAgentIds }).pipe(Effect.tap((res) => {
360
- const agents = res.agents;
389
+ ? this.sendRpc(AgentsLookup, { agentIds: unknownAgentIds }).pipe(Effect.tap((res) => {
361
390
  return Ref.update(this.agentNamesRef, (names) => {
362
391
  let next = names;
363
- for (const a of agents) {
392
+ for (const a of res.agents) {
364
393
  next = HashMap.set(next, a.id, a.name);
365
394
  }
366
395
  return next;
367
396
  });
368
397
  }), Effect.asVoid, Effect.catchAll(() => Effect.void))
369
398
  : Effect.void;
370
- const metaEff = this.sendRpc(ConversationsGet.name, {
371
- conversationId: convId,
372
- }).pipe(Effect.map((res) => res
373
- .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)));
374
402
  const [, convMeta] = yield* Effect.all([lookupEff, metaEff], {
375
- concurrency: "unbounded",
403
+ concurrency: HISTORY_LOOKUP_CONCURRENCY,
376
404
  });
377
405
  // Determine what's "new" using lastRead (not lastNotified).
378
406
  // lastNotified is advanced by getContext() (system-reminder).
@@ -446,7 +474,7 @@ export class MoltZapService {
446
474
  const cached = Option.getOrUndefined(HashMap.get(snapshot(this.agentNamesRef), agentId));
447
475
  if (cached !== undefined)
448
476
  return cached;
449
- return yield* this.sendRpc(AgentsLookup.name, {
477
+ return yield* this.sendRpc(AgentsLookup, {
450
478
  agentIds: [agentId],
451
479
  }).pipe(Effect.flatMap((result) => {
452
480
  const agent = result.agents[0];
@@ -464,33 +492,43 @@ export class MoltZapService {
464
492
  message: "Conversation is archived",
465
493
  }));
466
494
  }
467
- return Effect.asVoid(this.sendRpc(MessagesSend.name, {
468
- conversationId: convId,
495
+ return Effect.asVoid(this.sendRpc(MessagesSend, {
496
+ conversationId: toConversationId(convId),
469
497
  parts: [{ type: "text", text }],
470
- ...(opts?.replyTo ? { replyToId: opts.replyTo } : {}),
498
+ ...(opts?.replyTo ? { replyToId: toMessageId(opts.replyTo) } : {}),
471
499
  ...(opts?.dispatchLeaseId
472
500
  ? { dispatchLeaseId: opts.dispatchLeaseId }
473
501
  : {}),
474
502
  }));
475
503
  }
476
504
  authorizeDispatch(request) {
477
- return this.sendRpc(AppsAuthorizeDispatch.name, {
478
- conversationId: request.conversationId,
479
- messageId: request.message.id,
480
- senderAgentId: request.senderAgentId,
481
- 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],
482
510
  receivedAt: request.receivedAt,
483
511
  clock: request.clock,
484
- 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
+ })),
485
519
  attempt: request.attempt,
486
520
  }, { timeoutMs: DISPATCH_AUTHORIZATION_RPC_TIMEOUT_MS }).pipe(Effect.map((result) => {
487
- const admission = result.admission;
521
+ const { admission } = result;
488
522
  switch (admission.decision) {
489
523
  case "grant":
490
524
  return {
491
525
  _tag: "grant",
492
- leaseId: admission.leaseId,
493
- leaseTimeoutMs: admission.leaseTimeoutMs,
526
+ ...(admission.leaseId !== undefined
527
+ ? { leaseId: admission.leaseId }
528
+ : {}),
529
+ ...(admission.leaseTimeoutMs !== undefined
530
+ ? { leaseTimeoutMs: admission.leaseTimeoutMs }
531
+ : {}),
494
532
  ...(admission.dispatchMessageId
495
533
  ? { dispatchMessageId: admission.dispatchMessageId }
496
534
  : {}),
@@ -498,12 +536,16 @@ export class MoltZapService {
498
536
  case "deny":
499
537
  return {
500
538
  _tag: "deny",
501
- reason: admission.reason,
539
+ ...(admission.reason !== undefined
540
+ ? { reason: admission.reason }
541
+ : {}),
502
542
  };
503
543
  case "hold":
504
544
  return {
505
545
  _tag: "hold",
506
- reason: admission.reason,
546
+ ...(admission.reason !== undefined
547
+ ? { reason: admission.reason }
548
+ : {}),
507
549
  };
508
550
  }
509
551
  }));
@@ -513,17 +555,17 @@ export class MoltZapService {
513
555
  const cache = yield* Ref.get(this.agentConversationCacheRef);
514
556
  let conversationId = Option.getOrUndefined(HashMap.get(cache, agentName));
515
557
  if (!conversationId) {
516
- const lookupResult = (yield* this.sendRpc(AgentsLookupByName.name, {
558
+ const lookupResult = yield* this.sendRpc(AgentsLookupByName, {
517
559
  names: [agentName],
518
- }));
560
+ });
519
561
  const agent = lookupResult.agents[0];
520
562
  if (!agent) {
521
563
  return yield* Effect.fail(new AgentNotFoundError({ agentName }));
522
564
  }
523
- const createResult = (yield* this.sendRpc(ConversationsCreate.name, {
565
+ const createResult = yield* this.sendRpc(ConversationsCreate, {
524
566
  type: "dm",
525
- participants: [{ type: "agent", id: agent.id }],
526
- }));
567
+ participants: [{ type: "agent", id: toAgentId(agent.id) }],
568
+ });
527
569
  conversationId = createResult.conversation.id;
528
570
  const newId = conversationId;
529
571
  yield* Ref.update(this.agentConversationCacheRef, (m) => HashMap.set(m, agentName, newId));
@@ -553,8 +595,8 @@ export class MoltZapService {
553
595
  * committing can re-peek idempotently.
554
596
  */
555
597
  peekContextEntries(currentConvId, opts) {
556
- const maxConvs = opts?.maxConversations ?? 5;
557
- const maxMsgsPerConv = opts?.maxMessagesPerConv ?? 3;
598
+ const maxConvs = opts?.maxConversations ?? DEFAULT_MAX_CONTEXT_CONVERSATIONS;
599
+ const maxMsgsPerConv = opts?.maxMessagesPerConv ?? DEFAULT_MAX_MESSAGES_PER_CONVERSATION;
558
600
  const { messagesMap, conversationsMap, agentNamesMap, viewMarkers } = this.readCrossConvState(currentConvId);
559
601
  // Collect all candidate conversations with their last-message timestamp,
560
602
  // then sort by recency before applying `maxConvs`. HashMap iteration
@@ -577,7 +619,8 @@ export class MoltZapService {
577
619
  const reportable = newMsgs.slice(-maxMsgsPerConv);
578
620
  const last = reportable[reportable.length - 1];
579
621
  const senderName = getOr(agentNamesMap, last.senderId, () => last.senderId);
580
- const minutesAgo = Math.max(0, Math.round((Date.now() - new Date(last.createdAt).getTime()) / 60_000));
622
+ const minutesAgo = Math.max(0, Math.round((Date.now() - new Date(last.createdAt).getTime()) /
623
+ MILLISECONDS_PER_MINUTE));
581
624
  entries.push({
582
625
  conversationId: convId,
583
626
  conversationName: Option.getOrUndefined(HashMap.get(conversationsMap, convId))?.name,
@@ -656,57 +699,37 @@ export class MoltZapService {
656
699
  return HashMap.set(outer, currentConvId, markers);
657
700
  }));
658
701
  }
659
- on(event, handler) {
660
- switch (event) {
661
- case "message":
662
- this.messageHandlers.push(handler);
663
- break;
664
- case "rawEvent":
665
- this.rawEventHandlers.push(handler);
666
- break;
667
- case "disconnect":
668
- this.disconnectHandlers.push(handler);
669
- break;
670
- case "reconnect":
671
- this.reconnectHandlers.push(handler);
672
- break;
673
- case "permissionRequired":
674
- this.permissionRequiredHandlers.push(handler);
675
- break;
676
- case "conversationArchived":
677
- this.conversationArchivedHandlers.push(handler);
678
- break;
679
- case "conversationUnarchived":
680
- this.conversationUnarchivedHandlers.push(handler);
681
- break;
682
- }
702
+ // --- Notifications ---
703
+ on(notification, handler) {
704
+ this.handlers[notification].push(handler);
683
705
  }
684
706
  // --- RPC passthrough ---
685
- sendRpc(method, params, opts) {
707
+ sendRpc(definition, params, opts) {
686
708
  return Effect.suspend(() => {
687
709
  if (!this.client) {
688
710
  return Effect.fail(new NotConnectedError({ message: "Not connected" }));
689
711
  }
690
- return this.client.sendRpc(method, params, opts);
712
+ return this.client.sendRpc(definition, params, opts);
691
713
  });
692
714
  }
693
- grantPermission(params) {
694
- return Effect.asVoid(this.sendRpc(PermissionsGrant.name, params));
695
- }
696
715
  // --- Internals ---
697
716
  /**
698
717
  * Fetch full conversation details (including participants) via RPC and merge
699
- * into the local cache. Called on ConversationCreated events because the
700
- * 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.
701
721
  */
702
722
  refreshConversationParticipants(conversationId) {
703
- return this.sendRpc(ConversationsGet.name, { conversationId }).pipe(Effect.tap((res) => {
704
- const typed = res;
723
+ return this.sendRpc(ConversationsGet, {
724
+ conversationId: toConversationId(conversationId),
725
+ }).pipe(Effect.tap((res) => {
705
726
  const meta = {
706
- id: typed.conversation.id,
707
- type: typed.conversation.type,
708
- name: typed.conversation.name,
709
- 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
+ : {}),
710
733
  };
711
734
  if (this.archivedConversationIds.has(conversationId)) {
712
735
  return Effect.void;
@@ -727,92 +750,88 @@ export class MoltZapService {
727
750
  next = HashMap.set(next, conv.id, {
728
751
  id: conv.id,
729
752
  type: conv.type,
730
- name: conv.name,
731
753
  participants: (conv.participants ?? []).map((p) => `${p.type}:${p.id}`),
754
+ ...(conv.name !== undefined ? { name: conv.name } : {}),
732
755
  });
733
756
  }
734
757
  return next;
735
758
  }));
736
759
  }
737
- handleEvent(event) {
738
- const eventData = isPlainRecord(event.data) ? event.data : {};
739
- const message = isPlainRecord(eventData["message"])
740
- ? eventData["message"]
760
+ handleNotification(notification) {
761
+ const params = isPlainRecord(notification.params)
762
+ ? notification.params
763
+ : {};
764
+ const message = isPlainRecord(params["message"])
765
+ ? params["message"]
741
766
  : undefined;
742
- const conversation = isPlainRecord(eventData["conversation"])
743
- ? eventData["conversation"]
767
+ const conversation = isPlainRecord(params["conversation"])
768
+ ? params["conversation"]
744
769
  : undefined;
745
- const eventConversationId = typeof eventData["conversationId"] === "string"
746
- ? eventData["conversationId"]
770
+ const notificationConversationId = typeof params["conversationId"] === "string"
771
+ ? params["conversationId"]
747
772
  : undefined;
748
773
  appendClientEventTrace({
749
774
  ts: new Date().toISOString(),
750
775
  agentId: this._ownAgentId ?? "unknown",
751
- event: event.event,
776
+ notification: notification.method,
752
777
  messageId: message?.["id"],
753
778
  messageConversationId: message?.["conversationId"],
754
779
  messageSenderId: message?.["senderId"],
755
- conversationId: conversation?.["id"] ?? eventConversationId,
780
+ conversationId: conversation?.["id"] ?? notificationConversationId,
756
781
  conversationName: conversation?.["name"],
757
782
  });
758
- fanout(this.rawEventHandlers, event, this.opts.logger);
759
- // The server validates event.data against each event's schema before
760
- // emitting; each case casts to the typed Static<> payload for that
761
- // specific event.
762
- switch (event.event) {
763
- case EventNames.MessageReceived: {
764
- const msg = event.data.message;
765
- this.storeMessage(msg);
766
- // Name resolution is driven lazily by channel-core's serialized
767
- // consumer via resolveAgentName(), which populates agentNamesRef on
768
- // first miss and hits the cache on every subsequent message.
769
- if (msg.senderId !== this._ownAgentId) {
770
- fanout(this.messageHandlers, msg, this.opts.logger);
771
- }
772
- break;
773
- }
774
- case EventNames.PermissionsRequired: {
775
- fanout(this.permissionRequiredHandlers, event.data, this.opts.logger);
776
- break;
777
- }
778
- case EventNames.ConversationCreated:
779
- case EventNames.ConversationUpdated: {
780
- const { conversation } = event.data;
781
- this.archivedConversationIds.delete(conversation.id);
782
- Effect.runSync(Ref.update(this.conversationsRef, (m) => {
783
- const existing = Option.getOrUndefined(HashMap.get(m, conversation.id));
784
- return HashMap.set(m, conversation.id, {
785
- id: conversation.id,
786
- type: conversation.type,
787
- name: conversation.name,
788
- participants: existing?.participants ?? [],
789
- });
790
- }));
791
- // The ConversationCreated event schema doesn't carry participants
792
- // (protocol/schema/events.ts: ConversationSchema is id/type/name/
793
- // createdBy/timestamps only). Fetch full details asynchronously so
794
- // downstream code that reads getConversation(id).participants sees
795
- // a populated list within a round-trip of the event.
796
- if (event.event === EventNames.ConversationCreated) {
797
- // Fire-and-forget: refreshConversationParticipants never fails.
798
- Effect.runFork(this.refreshConversationParticipants(conversation.id));
799
- }
800
- break;
801
- }
802
- case EventNames.ConversationArchived: {
803
- const data = event.data;
804
- this.markConversationArchived(data.conversationId);
805
- fanout(this.conversationArchivedHandlers, data, this.opts.logger);
806
- break;
807
- }
808
- case EventNames.ConversationUnarchived: {
809
- const data = event.data;
810
- this.archivedConversationIds.delete(data.conversationId);
811
- fanout(this.conversationUnarchivedHandlers, data, this.opts.logger);
812
- break;
813
- }
783
+ fanout(this.handlers.rawNotification, notification, this.opts.logger);
784
+ if (isDecodedNotificationInGroup(serviceNotificationGroup, notification)) {
785
+ Effect.runSync(this.notificationHandlers.dispatch(notification));
814
786
  }
815
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
+ }
816
835
  markConversationArchived(conversationId) {
817
836
  this.archivedConversationIds.add(conversationId);
818
837
  Effect.runSync(Effect.all([