@moltzap/client 2026.511.1 → 2026.519.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 (218) hide show
  1. package/dist/auth.d.ts +12 -6
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +67 -29
  4. package/dist/auth.js.map +1 -1
  5. package/dist/auth.test.js +113 -76
  6. package/dist/auth.test.js.map +1 -1
  7. package/dist/channel-base/format-cross-conv.d.ts +43 -0
  8. package/dist/channel-base/format-cross-conv.d.ts.map +1 -0
  9. package/dist/channel-base/format-cross-conv.js +69 -0
  10. package/dist/channel-base/format-cross-conv.js.map +1 -0
  11. package/dist/channel-base/format-group-block.d.ts +43 -0
  12. package/dist/channel-base/format-group-block.d.ts.map +1 -0
  13. package/dist/channel-base/format-group-block.js +59 -0
  14. package/dist/channel-base/format-group-block.js.map +1 -0
  15. package/dist/channel-base/index.d.ts +21 -0
  16. package/dist/channel-base/index.d.ts.map +1 -0
  17. package/dist/channel-base/index.js +24 -0
  18. package/dist/channel-base/index.js.map +1 -0
  19. package/dist/channel-base/lease-guard.d.ts +30 -0
  20. package/dist/channel-base/lease-guard.d.ts.map +1 -0
  21. package/dist/channel-base/lease-guard.js +40 -0
  22. package/dist/channel-base/lease-guard.js.map +1 -0
  23. package/dist/channel-base/lease-store.d.ts +30 -0
  24. package/dist/channel-base/lease-store.d.ts.map +1 -0
  25. package/dist/channel-base/lease-store.js +58 -0
  26. package/dist/channel-base/lease-store.js.map +1 -0
  27. package/dist/channel-base/lease.d.ts +94 -0
  28. package/dist/channel-base/lease.d.ts.map +1 -0
  29. package/dist/channel-base/lease.js +110 -0
  30. package/dist/channel-base/lease.js.map +1 -0
  31. package/dist/channel-core-context.test.d.ts +2 -0
  32. package/dist/channel-core-context.test.d.ts.map +1 -0
  33. package/dist/channel-core-context.test.js +334 -0
  34. package/dist/channel-core-context.test.js.map +1 -0
  35. package/dist/channel-core-dispatch.test.d.ts +2 -0
  36. package/dist/channel-core-dispatch.test.d.ts.map +1 -0
  37. package/dist/channel-core-dispatch.test.js +550 -0
  38. package/dist/channel-core-dispatch.test.js.map +1 -0
  39. package/dist/channel-core-enrichment.d.ts +8 -0
  40. package/dist/channel-core-enrichment.d.ts.map +1 -0
  41. package/dist/channel-core-enrichment.js +134 -0
  42. package/dist/channel-core-enrichment.js.map +1 -0
  43. package/dist/channel-core-errors.d.ts +20 -0
  44. package/dist/channel-core-errors.d.ts.map +1 -0
  45. package/dist/channel-core-errors.js +12 -0
  46. package/dist/channel-core-errors.js.map +1 -0
  47. package/dist/channel-core-test-support.d.ts +48 -0
  48. package/dist/channel-core-test-support.d.ts.map +1 -0
  49. package/dist/channel-core-test-support.js +111 -0
  50. package/dist/channel-core-test-support.js.map +1 -0
  51. package/dist/channel-core.d.ts +30 -7
  52. package/dist/channel-core.d.ts.map +1 -1
  53. package/dist/channel-core.js +214 -237
  54. package/dist/channel-core.js.map +1 -1
  55. package/dist/channel-core.test.js +216 -1082
  56. package/dist/channel-core.test.js.map +1 -1
  57. package/dist/cli/__tests__/cli-multi-agent.int.test.js +129 -23
  58. package/dist/cli/__tests__/cli-multi-agent.int.test.js.map +1 -1
  59. package/dist/cli/commands/conversations-v2.test.js +64 -54
  60. package/dist/cli/commands/conversations-v2.test.js.map +1 -1
  61. package/dist/cli/commands/conversations.d.ts +5 -5
  62. package/dist/cli/commands/conversations.d.ts.map +1 -1
  63. package/dist/cli/commands/conversations.js +43 -36
  64. package/dist/cli/commands/conversations.js.map +1 -1
  65. package/dist/cli/commands/messages.d.ts +4 -4
  66. package/dist/cli/commands/messages.d.ts.map +1 -1
  67. package/dist/cli/commands/messages.js +4 -4
  68. package/dist/cli/commands/messages.js.map +1 -1
  69. package/dist/cli/commands/messages.test.js +61 -49
  70. package/dist/cli/commands/messages.test.js.map +1 -1
  71. package/dist/cli/commands/ping.d.ts +0 -9
  72. package/dist/cli/commands/ping.d.ts.map +1 -1
  73. package/dist/cli/commands/ping.js +1 -1
  74. package/dist/cli/commands/ping.js.map +1 -1
  75. package/dist/cli/commands/presence.d.ts +1 -1
  76. package/dist/cli/commands/presence.js +1 -1
  77. package/dist/cli/commands/register.d.ts +2 -2
  78. package/dist/cli/commands/register.d.ts.map +1 -1
  79. package/dist/cli/commands/register.js +98 -77
  80. package/dist/cli/commands/register.js.map +1 -1
  81. package/dist/cli/commands/register.test.js +63 -39
  82. package/dist/cli/commands/register.test.js.map +1 -1
  83. package/dist/cli/commands/send.d.ts +6 -6
  84. package/dist/cli/commands/send.js +6 -6
  85. package/dist/cli/commands/send.test.js +32 -28
  86. package/dist/cli/commands/send.test.js.map +1 -1
  87. package/dist/cli/commands/status.d.ts.map +1 -1
  88. package/dist/cli/commands/status.js +3 -4
  89. package/dist/cli/commands/status.js.map +1 -1
  90. package/dist/cli/config.d.ts +0 -2
  91. package/dist/cli/config.d.ts.map +1 -1
  92. package/dist/cli/config.js +86 -76
  93. package/dist/cli/config.js.map +1 -1
  94. package/dist/cli/config.test.js +80 -42
  95. package/dist/cli/config.test.js.map +1 -1
  96. package/dist/cli/index.d.ts +1 -16
  97. package/dist/cli/index.d.ts.map +1 -1
  98. package/dist/cli/index.js +50 -113
  99. package/dist/cli/index.js.map +1 -1
  100. package/dist/cli/process-argv.d.ts +2 -0
  101. package/dist/cli/process-argv.d.ts.map +1 -0
  102. package/dist/cli/process-argv.js +2 -0
  103. package/dist/cli/process-argv.js.map +1 -0
  104. package/dist/cli/profile.d.ts +3 -10
  105. package/dist/cli/profile.d.ts.map +1 -1
  106. package/dist/cli/profile.js +105 -104
  107. package/dist/cli/profile.js.map +1 -1
  108. package/dist/cli/profile.test.js +338 -232
  109. package/dist/cli/profile.test.js.map +1 -1
  110. package/dist/cli/runtime.d.ts +5 -17
  111. package/dist/cli/runtime.d.ts.map +1 -1
  112. package/dist/cli/runtime.js +6 -59
  113. package/dist/cli/runtime.js.map +1 -1
  114. package/dist/cli/socket-client.d.ts +10 -15
  115. package/dist/cli/socket-client.d.ts.map +1 -1
  116. package/dist/cli/socket-client.js +74 -89
  117. package/dist/cli/socket-client.js.map +1 -1
  118. package/dist/cli/transport.d.ts +9 -5
  119. package/dist/cli/transport.d.ts.map +1 -1
  120. package/dist/cli/transport.js +45 -42
  121. package/dist/cli/transport.js.map +1 -1
  122. package/dist/cli/transport.test.js +227 -133
  123. package/dist/cli/transport.test.js.map +1 -1
  124. package/dist/index.d.ts +4 -1
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +3 -0
  127. package/dist/index.js.map +1 -1
  128. package/dist/local-daemon-rpc.d.ts +13 -0
  129. package/dist/local-daemon-rpc.d.ts.map +1 -0
  130. package/dist/local-daemon-rpc.js +17 -0
  131. package/dist/local-daemon-rpc.js.map +1 -0
  132. package/dist/local-paths.d.ts +7 -0
  133. package/dist/local-paths.d.ts.map +1 -0
  134. package/dist/local-paths.js +20 -0
  135. package/dist/local-paths.js.map +1 -0
  136. package/dist/runtime/close-info.d.ts +3 -12
  137. package/dist/runtime/close-info.d.ts.map +1 -1
  138. package/dist/runtime/close-info.js +28 -39
  139. package/dist/runtime/close-info.js.map +1 -1
  140. package/dist/runtime/close-info.test.js +77 -70
  141. package/dist/runtime/close-info.test.js.map +1 -1
  142. package/dist/runtime/errors.test.js +42 -28
  143. package/dist/runtime/errors.test.js.map +1 -1
  144. package/dist/runtime/frame.d.ts +4 -31
  145. package/dist/runtime/frame.d.ts.map +1 -1
  146. package/dist/runtime/frame.js +7 -116
  147. package/dist/runtime/frame.js.map +1 -1
  148. package/dist/runtime/frame.test.js +41 -54
  149. package/dist/runtime/frame.test.js.map +1 -1
  150. package/dist/runtime/index.d.ts +7 -0
  151. package/dist/runtime/index.d.ts.map +1 -1
  152. package/dist/runtime/index.js +7 -11
  153. package/dist/runtime/index.js.map +1 -1
  154. package/dist/runtime/local-history.d.ts +67 -0
  155. package/dist/runtime/local-history.d.ts.map +1 -0
  156. package/dist/runtime/local-history.js +77 -0
  157. package/dist/runtime/local-history.js.map +1 -0
  158. package/dist/runtime/local-service-commands.d.ts +19 -0
  159. package/dist/runtime/local-service-commands.d.ts.map +1 -1
  160. package/dist/runtime/local-service-commands.js +23 -0
  161. package/dist/runtime/local-service-commands.js.map +1 -1
  162. package/dist/runtime/local-socket-server.d.ts +20 -0
  163. package/dist/runtime/local-socket-server.d.ts.map +1 -0
  164. package/dist/runtime/local-socket-server.js +94 -0
  165. package/dist/runtime/local-socket-server.js.map +1 -0
  166. package/dist/runtime/refs.d.ts +1 -1
  167. package/dist/runtime/refs.js +1 -1
  168. package/dist/runtime/runtime.test.js +29 -21
  169. package/dist/runtime/runtime.test.js.map +1 -1
  170. package/dist/runtime/service-helpers.d.ts +32 -0
  171. package/dist/runtime/service-helpers.d.ts.map +1 -0
  172. package/dist/runtime/service-helpers.js +85 -0
  173. package/dist/runtime/service-helpers.js.map +1 -0
  174. package/dist/runtime/subscribers.d.ts +13 -17
  175. package/dist/runtime/subscribers.d.ts.map +1 -1
  176. package/dist/runtime/subscribers.js +37 -32
  177. package/dist/runtime/subscribers.js.map +1 -1
  178. package/dist/runtime/subscribers.test.js +139 -131
  179. package/dist/runtime/subscribers.test.js.map +1 -1
  180. package/dist/service-socket-path.test.d.ts +2 -0
  181. package/dist/service-socket-path.test.d.ts.map +1 -0
  182. package/dist/service-socket-path.test.js +87 -0
  183. package/dist/service-socket-path.test.js.map +1 -0
  184. package/dist/service.d.ts +23 -20
  185. package/dist/service.d.ts.map +1 -1
  186. package/dist/service.js +192 -340
  187. package/dist/service.js.map +1 -1
  188. package/dist/service.test.js +652 -653
  189. package/dist/service.test.js.map +1 -1
  190. package/dist/test/index.d.ts +30 -14
  191. package/dist/test/index.d.ts.map +1 -1
  192. package/dist/test/index.js +25 -21
  193. package/dist/test/index.js.map +1 -1
  194. package/dist/test-utils/channel-service-fixture.d.ts +1 -0
  195. package/dist/test-utils/channel-service-fixture.d.ts.map +1 -1
  196. package/dist/test-utils/channel-service-fixture.js +139 -98
  197. package/dist/test-utils/channel-service-fixture.js.map +1 -1
  198. package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
  199. package/dist/test-utils/conformance-adapter.js +145 -124
  200. package/dist/test-utils/conformance-adapter.js.map +1 -1
  201. package/dist/test-utils/fake-service.d.ts.map +1 -1
  202. package/dist/test-utils/fake-service.js +3 -3
  203. package/dist/test-utils/fake-service.js.map +1 -1
  204. package/dist/test-utils/index.d.ts +25 -0
  205. package/dist/test-utils/index.d.ts.map +1 -1
  206. package/dist/test-utils/index.js +25 -8
  207. package/dist/test-utils/index.js.map +1 -1
  208. package/dist/ws-client-test-support.d.ts +180 -0
  209. package/dist/ws-client-test-support.d.ts.map +1 -0
  210. package/dist/ws-client-test-support.js +399 -0
  211. package/dist/ws-client-test-support.js.map +1 -0
  212. package/dist/ws-client.d.ts +59 -32
  213. package/dist/ws-client.d.ts.map +1 -1
  214. package/dist/ws-client.js +272 -321
  215. package/dist/ws-client.js.map +1 -1
  216. package/dist/ws-client.test.js +388 -800
  217. package/dist/ws-client.test.js.map +1 -1
  218. package/package.json +18 -10
@@ -2,27 +2,12 @@
2
2
  * Shared message-enrichment helper for MoltZap channel adapters.
3
3
  */
4
4
  import { Cause, Chunk, Deferred, Duration, Effect, Fiber, Queue } from "effect";
5
- const noopLogger = {
6
- info: () => { },
7
- warn: () => { },
8
- error: () => { },
9
- };
5
+ import { DispatchAdmissionTimedOut, DispatchLeaseExpired, } from "./channel-core-errors.js";
6
+ import { enrichChannelMessage } from "./channel-core-enrichment.js";
10
7
  const DEFAULT_DISPATCH_ADMISSION_TIMEOUT_MS = 30_000;
11
8
  const DEFAULT_DISPATCH_LEASE_TIMEOUT_MS = 90_000;
12
9
  const DISPATCH_RELEASE_RING_CAPACITY = 256;
13
10
  const DISPATCH_RELEASE_RING_SOFT_TTL_MS = 30_000;
14
- class DispatchLeaseExpired extends Error {
15
- messageId;
16
- conversationId;
17
- timeoutMs;
18
- constructor(messageId, conversationId, timeoutMs) {
19
- super(`dispatch lease expired after ${timeoutMs}ms`);
20
- this.messageId = messageId;
21
- this.conversationId = conversationId;
22
- this.timeoutMs = timeoutMs;
23
- this.name = "DispatchLeaseExpired";
24
- }
25
- }
26
11
  function errorSummary(err) {
27
12
  if (err instanceof Error) {
28
13
  return {
@@ -35,11 +20,17 @@ function errorSummary(err) {
35
20
  errorValue: String(err),
36
21
  };
37
22
  }
38
- function extractTextContent(parts) {
39
- return parts
40
- .filter((p) => p.type === "text")
41
- .map((p) => p.text)
42
- .join("\n");
23
+ function effectLogInfo(message, annotations) {
24
+ return Effect.logInfo(message).pipe(Effect.annotateLogs(annotations));
25
+ }
26
+ function effectLogWarning(message, annotations) {
27
+ return Effect.logWarning(message).pipe(Effect.annotateLogs(annotations));
28
+ }
29
+ function effectLogError(message, annotations) {
30
+ return Effect.logError(message).pipe(Effect.annotateLogs(annotations));
31
+ }
32
+ function runBackgroundLog(effect) {
33
+ Effect.runFork(effect);
43
34
  }
44
35
  /**
45
36
  * Wraps a MoltZapService with message enrichment, dispatch-chain ordering,
@@ -51,7 +42,6 @@ function extractTextContent(parts) {
51
42
  */
52
43
  export class MoltZapChannelCore {
53
44
  service;
54
- logger;
55
45
  dispatchAdmissionTimeoutMs;
56
46
  connected = false;
57
47
  inboundHandler = null;
@@ -82,23 +72,31 @@ export class MoltZapChannelCore {
82
72
  closedConversationIds = new Set();
83
73
  logicalClocks = new Map();
84
74
  parkedByConversation = new Map();
85
- /** Inbound messages enqueue synchronously; a single forked consumer fiber
86
- * serialises delivery so handlers execute one-at-a-time in arrival order. */
75
+ /**
76
+ * Inbound messages enqueue synchronously; a single forked consumer fiber
77
+ * serialises delivery so handlers execute one-at-a-time in arrival order.
78
+ */
87
79
  inboundQueue = Effect.runSync(Queue.unbounded());
88
80
  consumerFiber;
89
81
  disconnectHandlers = [];
90
82
  reconnectHandlers = [];
91
83
  constructor(opts) {
92
84
  this.service = opts.service;
93
- this.logger = opts.logger ?? noopLogger;
94
85
  this.dispatchAdmissionTimeoutMs =
95
86
  opts.dispatchAdmissionTimeoutMs ?? DEFAULT_DISPATCH_ADMISSION_TIMEOUT_MS;
87
+ this.registerMessageListener();
88
+ this.consumerFiber = this.startConsumerFiber();
89
+ this.registerConnectionListeners();
90
+ this.registerConversationLifecycleListeners();
91
+ this.registerDispatchReleaseListener();
92
+ }
93
+ registerMessageListener() {
96
94
  this.service.on("message", (message) => {
97
95
  if (this.closedConversationIds.has(message.conversationId)) {
98
- this.logger.info({
96
+ runBackgroundLog(effectLogInfo("MoltZapChannelCore: dropping inbound message for closed conversation", {
99
97
  messageId: message.id,
100
98
  conversationId: message.conversationId,
101
- }, "MoltZapChannelCore: dropping inbound message for closed conversation");
99
+ }));
102
100
  return;
103
101
  }
104
102
  Queue.unsafeOffer(this.inboundQueue, {
@@ -108,17 +106,20 @@ export class MoltZapChannelCore {
108
106
  clock: this.observeMessage(message),
109
107
  });
110
108
  });
111
- // Both typed failures (Effect.fail) and defects (sync throws inside the
112
- // handler's Effect) are caught — Cause.squash collapses either into a
113
- // single value for the logger so the consumer fiber survives a misbehaving
114
- // handler in either mode.
115
- const consumer = Effect.forever(Queue.take(this.inboundQueue).pipe(Effect.flatMap((work) => this.dispatchInboundWork(work).pipe(Effect.catchAllCause((cause) => Effect.sync(() => this.logger.error({
109
+ }
110
+ startConsumerFiber() {
111
+ const consumer = Effect.forever(Queue.take(this.inboundQueue).pipe(Effect.flatMap((work) => this.dispatchInboundWork(work).pipe(Effect.catchAllCause((cause) => this.logInboundFailure(work, cause))))));
112
+ return Effect.runFork(consumer);
113
+ }
114
+ logInboundFailure(work, cause) {
115
+ return effectLogError("MoltZapChannelCore: inbound handler failed", {
116
116
  messageId: work.message.id,
117
117
  conversationId: work.message.conversationId,
118
118
  causePretty: Cause.pretty(cause),
119
119
  ...errorSummary(Cause.squash(cause)),
120
- }, "MoltZapChannelCore: inbound handler failed")))))));
121
- this.consumerFiber = Effect.runFork(consumer);
120
+ });
121
+ }
122
+ registerConnectionListeners() {
122
123
  this.service.on("disconnect", () => {
123
124
  this.connected = false;
124
125
  this.fanout(this.disconnectHandlers, "disconnect");
@@ -127,12 +128,16 @@ export class MoltZapChannelCore {
127
128
  this.connected = true;
128
129
  this.fanout(this.reconnectHandlers, "reconnect");
129
130
  });
131
+ }
132
+ registerConversationLifecycleListeners() {
130
133
  this.service.on("conversationArchived", ({ conversationId }) => {
131
134
  this.closeConversation(conversationId);
132
135
  });
133
136
  this.service.on("conversationUnarchived", ({ conversationId }) => {
134
137
  this.closedConversationIds.delete(conversationId);
135
138
  });
139
+ }
140
+ registerDispatchReleaseListener() {
136
141
  this.service.on("dispatchRelease", (frame) => {
137
142
  this.recordDispatchRelease(frame);
138
143
  });
@@ -170,7 +175,7 @@ export class MoltZapChannelCore {
170
175
  if (oldest === undefined)
171
176
  break;
172
177
  this.pendingReleasesByLease.delete(oldest);
173
- this.logger.warn({ leaseId: oldest }, "MoltZapChannelCore: dispatchRelease ring buffer evicted oldest entry (capacity reached)");
178
+ runBackgroundLog(effectLogWarning("MoltZapChannelCore: dispatchRelease ring buffer evicted oldest entry (capacity reached)", { leaseId: oldest }));
174
179
  }
175
180
  }
176
181
  evictDispatchReleaseRing(nowMs) {
@@ -181,7 +186,7 @@ export class MoltZapChannelCore {
181
186
  return;
182
187
  }
183
188
  this.pendingReleasesByLease.delete(leaseId);
184
- this.logger.warn({ leaseId, ageMs: nowMs - entry.receivedAtMs }, "MoltZapChannelCore: dispatchRelease ring buffer evicted stale entry (soft TTL)");
189
+ runBackgroundLog(effectLogWarning("MoltZapChannelCore: dispatchRelease ring buffer evicted stale entry (soft TTL)", { leaseId, ageMs: nowMs - entry.receivedAtMs }));
185
190
  }
186
191
  }
187
192
  consumeDispatchRelease(leaseId) {
@@ -207,7 +212,10 @@ export class MoltZapChannelCore {
207
212
  h();
208
213
  }
209
214
  catch (err) {
210
- this.logger.error({ err, label }, `MoltZapChannelCore: ${label} handler threw`);
215
+ runBackgroundLog(effectLogError(`MoltZapChannelCore: ${label} handler threw`, {
216
+ err,
217
+ label,
218
+ }));
211
219
  }
212
220
  }
213
221
  }
@@ -231,7 +239,7 @@ export class MoltZapChannelCore {
231
239
  sendReply(conversationId, text, opts) {
232
240
  if (this.closedConversationIds.has(conversationId) ||
233
241
  this.service.isConversationArchived?.(conversationId)) {
234
- return Effect.sync(() => this.logger.info({ conversationId }, "MoltZapChannelCore: dropping reply for closed conversation"));
242
+ return effectLogInfo("MoltZapChannelCore: dropping reply for closed conversation", { conversationId });
235
243
  }
236
244
  return this.service.send(conversationId, text, {
237
245
  dispatchLeaseId: opts?.dispatchLeaseId ?? this.leaseIdInFlight,
@@ -250,10 +258,10 @@ export class MoltZapChannelCore {
250
258
  Queue.unsafeOffer(this.inboundQueue, work);
251
259
  }
252
260
  }
253
- this.logger.info({
261
+ runBackgroundLog(effectLogInfo("MoltZapChannelCore: closed conversation dispatch work purged", {
254
262
  conversationId,
255
263
  droppedQueued,
256
- }, "MoltZapChannelCore: closed conversation dispatch work purged");
264
+ }));
257
265
  }
258
266
  observeMessage(message) {
259
267
  const current = this.logicalClocks.get(message.conversationId) ?? {
@@ -337,14 +345,16 @@ export class MoltZapChannelCore {
337
345
  // timeouts independently.
338
346
  Effect.timeoutFail({
339
347
  duration: Duration.millis(this.dispatchAdmissionTimeoutMs),
340
- onTimeout: () => new Error(`dispatch request timed out after ${this.dispatchAdmissionTimeoutMs}ms`),
341
- }), Effect.flatMap(({ leaseId }) => this.awaitDispatchRelease(work, leaseId)), Effect.catchAll((err) => Effect.sync(() => {
342
- this.logger.warn({
348
+ onTimeout: () => new DispatchAdmissionTimedOut({
349
+ timeoutMs: this.dispatchAdmissionTimeoutMs,
350
+ }),
351
+ }), Effect.flatMap(({ leaseId }) => this.awaitDispatchRelease(work, leaseId)), Effect.catchAll((err) => Effect.gen(function* () {
352
+ yield* effectLogWarning("MoltZapChannelCore: dispatch admission failed closed", {
343
353
  messageId: work.message.id,
344
354
  conversationId: work.message.conversationId,
345
355
  attempt: work.attempt,
346
356
  err,
347
- }, "MoltZapChannelCore: dispatch admission failed closed");
357
+ });
348
358
  return {
349
359
  _tag: "deny",
350
360
  reason: "dispatch admission unavailable",
@@ -374,12 +384,12 @@ export class MoltZapChannelCore {
374
384
  this.pendingDispatchesByLease.delete(leaseId);
375
385
  })));
376
386
  if (settled._tag === "None") {
377
- this.logger.warn({
387
+ yield* effectLogWarning("MoltZapChannelCore: dispatchRelease wait timed out - fail-closed deny", {
378
388
  messageId: work.message.id,
379
389
  conversationId: work.message.conversationId,
380
390
  leaseId,
381
391
  timeoutMs: this.dispatchAdmissionTimeoutMs,
382
- }, "MoltZapChannelCore: dispatchRelease wait timed out — fail-closed deny");
392
+ });
383
393
  return {
384
394
  _tag: "deny",
385
395
  reason: "dispatch release wait timed out",
@@ -389,135 +399,184 @@ export class MoltZapChannelCore {
389
399
  });
390
400
  }
391
401
  projectVerdict(work, leaseId, verdict) {
392
- if (verdict.decision === "grant") {
393
- this.logger.info({
394
- messageId: work.message.id,
395
- conversationId: work.message.conversationId,
396
- attempt: work.attempt,
397
- leaseId,
398
- leaseTimeoutMs: verdict.leaseTimeoutMs,
399
- dispatchMessageId: verdict.dispatchMessageId,
400
- }, "MoltZapChannelCore: dispatch admission granted");
401
- const grant = {
402
- _tag: "grant",
403
- leaseId: verdict.leaseId ?? leaseId,
404
- ...(verdict.leaseTimeoutMs !== undefined
405
- ? { leaseTimeoutMs: verdict.leaseTimeoutMs }
406
- : {}),
407
- ...(verdict.dispatchMessageId
408
- ? { dispatchMessageId: verdict.dispatchMessageId }
409
- : {}),
410
- };
411
- return grant;
412
- }
413
- if (verdict.decision === "deny") {
414
- return {
415
- _tag: "deny",
416
- ...(verdict.reason !== undefined ? { reason: verdict.reason } : {}),
417
- };
402
+ switch (verdict.decision) {
403
+ case "grant":
404
+ return this.projectGrantVerdict(work, leaseId, verdict);
405
+ case "deny":
406
+ return MoltZapChannelCore.denyDecision(verdict.reason);
407
+ case "hold":
408
+ return MoltZapChannelCore.holdDecision(verdict.reason);
418
409
  }
410
+ }
411
+ projectGrantVerdict(work, leaseId, verdict) {
412
+ runBackgroundLog(effectLogInfo("MoltZapChannelCore: dispatch admission granted", {
413
+ messageId: work.message.id,
414
+ conversationId: work.message.conversationId,
415
+ attempt: work.attempt,
416
+ leaseId,
417
+ leaseTimeoutMs: verdict.leaseTimeoutMs,
418
+ dispatchMessageId: verdict.dispatchMessageId,
419
+ }));
420
+ return {
421
+ _tag: "grant",
422
+ leaseId: verdict.leaseId ?? leaseId,
423
+ ...(verdict.leaseTimeoutMs !== undefined
424
+ ? { leaseTimeoutMs: verdict.leaseTimeoutMs }
425
+ : {}),
426
+ ...(verdict.dispatchMessageId
427
+ ? { dispatchMessageId: verdict.dispatchMessageId }
428
+ : {}),
429
+ };
430
+ }
431
+ static denyDecision(reason) {
432
+ return {
433
+ _tag: "deny",
434
+ ...(reason !== undefined ? { reason } : {}),
435
+ };
436
+ }
437
+ static holdDecision(reason) {
419
438
  return {
420
439
  _tag: "hold",
421
- ...(verdict.reason !== undefined ? { reason: verdict.reason } : {}),
440
+ ...(reason !== undefined ? { reason } : {}),
422
441
  };
423
442
  }
424
443
  dispatchInboundWork(work) {
425
444
  return Effect.gen(this, function* () {
426
445
  const current = this.takeDispatchCandidate(work);
427
446
  if (this.closedConversationIds.has(current.message.conversationId)) {
428
- yield* Effect.sync(() => this.logger.info({
429
- messageId: current.message.id,
430
- conversationId: current.message.conversationId,
431
- attempt: current.attempt,
432
- }, "MoltZapChannelCore: dropping dispatch for closed conversation"));
447
+ yield* this.logClosedDispatchDrop(current);
433
448
  return;
434
449
  }
435
450
  const decision = yield* this.dispatchAdmission(current);
436
- if (decision._tag === "grant") {
437
- const messages = this.service.requestDispatch
438
- ? yield* this.takeCoalescedConversationMessages(current, decision.dispatchMessageId)
439
- : [current.message];
440
- if (messages.length === 0) {
441
- yield* Effect.sync(() => this.logger.warn({
442
- messageId: current.message.id,
443
- conversationId: current.message.conversationId,
444
- attempt: current.attempt,
445
- dispatchMessageId: decision.dispatchMessageId,
446
- }, "MoltZapChannelCore: dispatch admission target unavailable"));
447
- return;
448
- }
449
- const primaryMessage = messages[0];
450
- yield* Effect.sync(() => this.logger.info({
451
- messageId: primaryMessage.id,
452
- admittedMessageId: current.message.id,
453
- conversationId: current.message.conversationId,
454
- attempt: current.attempt,
455
- leaseId: decision.leaseId,
456
- coalescedMessageCount: messages.length,
457
- }, "MoltZapChannelCore: inbound dispatch starting"));
458
- const dispatch = Effect.acquireUseRelease(Effect.sync(() => {
459
- const previous = this.leaseIdInFlight;
460
- this.leaseIdInFlight = decision.leaseId;
461
- return previous;
462
- }), () => this.dispatchInboundEffect(messages), (previous) => Effect.sync(() => {
463
- this.leaseIdInFlight = previous;
464
- }));
465
- const timeoutMs = decision.leaseId
466
- ? (decision.leaseTimeoutMs ?? DEFAULT_DISPATCH_LEASE_TIMEOUT_MS)
467
- : undefined;
468
- let dispatchTimedOut = false;
469
- if (timeoutMs === undefined) {
470
- yield* dispatch;
471
- }
472
- else {
473
- yield* dispatch.pipe(Effect.timeoutFail({
474
- duration: Duration.millis(timeoutMs),
475
- onTimeout: () => new DispatchLeaseExpired(primaryMessage.id, primaryMessage.conversationId, timeoutMs),
476
- }), Effect.catchAll((err) => {
477
- if (err instanceof DispatchLeaseExpired) {
478
- return Effect.sync(() => {
479
- dispatchTimedOut = true;
480
- this.logger.warn({
481
- messageId: err.messageId,
482
- conversationId: err.conversationId,
483
- attempt: current.attempt,
484
- leaseId: decision.leaseId,
485
- timeoutMs: err.timeoutMs,
486
- }, "MoltZapChannelCore: inbound dispatch lease expired");
487
- });
488
- }
489
- return Effect.fail(err);
490
- }));
491
- }
492
- if (!dispatchTimedOut) {
493
- yield* Effect.sync(() => this.logger.info({
494
- messageId: primaryMessage.id,
495
- admittedMessageId: current.message.id,
496
- conversationId: current.message.conversationId,
497
- attempt: current.attempt,
498
- leaseId: decision.leaseId,
499
- }, "MoltZapChannelCore: inbound dispatch completed"));
500
- }
451
+ yield* this.handleDispatchDecision(current, decision);
452
+ });
453
+ }
454
+ handleDispatchDecision(current, decision) {
455
+ switch (decision._tag) {
456
+ case "grant":
457
+ return this.dispatchGrantedWork(current, decision);
458
+ case "deny":
459
+ return this.logDeniedDispatch(current, decision);
460
+ case "hold":
461
+ return this.holdDispatchWork(current, decision);
462
+ }
463
+ }
464
+ dispatchGrantedWork(current, decision) {
465
+ return Effect.gen(this, function* () {
466
+ const messages = yield* this.messagesForGrantedDispatch(current, decision);
467
+ if (messages.length === 0) {
468
+ yield* this.logDispatchTargetUnavailable(current, decision);
501
469
  return;
502
470
  }
503
- if (decision._tag === "deny") {
504
- yield* Effect.sync(() => this.logger.info({
505
- messageId: current.message.id,
506
- conversationId: current.message.conversationId,
507
- attempt: current.attempt,
508
- reason: decision.reason,
509
- }, "MoltZapChannelCore: inbound dispatch denied"));
510
- return;
471
+ const primaryMessage = messages[0];
472
+ yield* this.logDispatchStart(current, primaryMessage, messages, decision);
473
+ const timedOut = yield* this.runGrantedDispatch(current, primaryMessage, messages, decision);
474
+ if (!timedOut) {
475
+ yield* this.logDispatchCompleted(current, primaryMessage, decision);
511
476
  }
512
- yield* Effect.sync(() => this.logger.info({
477
+ });
478
+ }
479
+ messagesForGrantedDispatch(current, decision) {
480
+ return this.service.requestDispatch
481
+ ? this.takeCoalescedConversationMessages(current, decision.dispatchMessageId)
482
+ : Effect.succeed([current.message]);
483
+ }
484
+ runGrantedDispatch(current, primaryMessage, messages, decision) {
485
+ const dispatch = this.dispatchWithLease(messages, decision.leaseId);
486
+ const timeoutMs = MoltZapChannelCore.leaseTimeoutMs(decision);
487
+ if (timeoutMs === undefined)
488
+ return dispatch.pipe(Effect.as(false));
489
+ return dispatch.pipe(Effect.timeoutFail({
490
+ duration: Duration.millis(timeoutMs),
491
+ onTimeout: () => new DispatchLeaseExpired({
492
+ messageId: primaryMessage.id,
493
+ conversationId: primaryMessage.conversationId,
494
+ timeoutMs,
495
+ }),
496
+ }), Effect.as(false), Effect.catchAll((err) => this.handleDispatchFailure(err, current, decision)));
497
+ }
498
+ dispatchWithLease(messages, leaseId) {
499
+ return Effect.sync(() => {
500
+ const previous = this.leaseIdInFlight;
501
+ this.leaseIdInFlight = leaseId;
502
+ return previous;
503
+ }).pipe(Effect.flatMap((previous) => this.dispatchInboundEffect(messages).pipe(Effect.ensuring(Effect.sync(() => {
504
+ this.leaseIdInFlight = previous;
505
+ })))));
506
+ }
507
+ static leaseTimeoutMs(decision) {
508
+ return decision.leaseId
509
+ ? (decision.leaseTimeoutMs ?? DEFAULT_DISPATCH_LEASE_TIMEOUT_MS)
510
+ : undefined;
511
+ }
512
+ handleDispatchFailure(err, current, decision) {
513
+ if (err instanceof DispatchLeaseExpired) {
514
+ return this.logDispatchLeaseExpired(err, current, decision);
515
+ }
516
+ return Effect.fail(err);
517
+ }
518
+ holdDispatchWork(current, decision) {
519
+ return Effect.gen(this, function* () {
520
+ yield* effectLogInfo("MoltZapChannelCore: inbound dispatch held", {
513
521
  messageId: current.message.id,
514
522
  conversationId: current.message.conversationId,
515
523
  attempt: current.attempt,
516
524
  reason: decision.reason,
517
- }, "MoltZapChannelCore: inbound dispatch held"));
525
+ });
518
526
  this.parkDispatchWork(current);
519
527
  });
520
528
  }
529
+ logClosedDispatchDrop(current) {
530
+ return effectLogInfo("MoltZapChannelCore: dropping dispatch for closed conversation", {
531
+ messageId: current.message.id,
532
+ conversationId: current.message.conversationId,
533
+ attempt: current.attempt,
534
+ });
535
+ }
536
+ logDispatchTargetUnavailable(current, decision) {
537
+ return effectLogWarning("MoltZapChannelCore: dispatch admission target unavailable", {
538
+ messageId: current.message.id,
539
+ conversationId: current.message.conversationId,
540
+ attempt: current.attempt,
541
+ dispatchMessageId: decision.dispatchMessageId,
542
+ });
543
+ }
544
+ logDispatchStart(current, primaryMessage, messages, decision) {
545
+ return effectLogInfo("MoltZapChannelCore: inbound dispatch starting", {
546
+ messageId: primaryMessage.id,
547
+ admittedMessageId: current.message.id,
548
+ conversationId: current.message.conversationId,
549
+ attempt: current.attempt,
550
+ leaseId: decision.leaseId,
551
+ coalescedMessageCount: messages.length,
552
+ });
553
+ }
554
+ logDispatchLeaseExpired(err, current, decision) {
555
+ return effectLogWarning("MoltZapChannelCore: inbound dispatch lease expired", {
556
+ messageId: err.messageId,
557
+ conversationId: err.conversationId,
558
+ attempt: current.attempt,
559
+ leaseId: decision.leaseId,
560
+ timeoutMs: err.timeoutMs,
561
+ }).pipe(Effect.as(true));
562
+ }
563
+ logDispatchCompleted(current, primaryMessage, decision) {
564
+ return effectLogInfo("MoltZapChannelCore: inbound dispatch completed", {
565
+ messageId: primaryMessage.id,
566
+ admittedMessageId: current.message.id,
567
+ conversationId: current.message.conversationId,
568
+ attempt: current.attempt,
569
+ leaseId: decision.leaseId,
570
+ });
571
+ }
572
+ logDeniedDispatch(current, decision) {
573
+ return effectLogInfo("MoltZapChannelCore: inbound dispatch denied", {
574
+ messageId: current.message.id,
575
+ conversationId: current.message.conversationId,
576
+ attempt: current.attempt,
577
+ reason: decision.reason,
578
+ });
579
+ }
521
580
  pendingDispatchSnapshot(active) {
522
581
  const queued = Chunk.toReadonlyArray(Effect.runSync(Queue.takeAll(this.inboundQueue)));
523
582
  for (const work of queued) {
@@ -568,89 +627,7 @@ export class MoltZapChannelCore {
568
627
  * `resolveAgentName` throws (e.g. service not yet connected).
569
628
  */
570
629
  static enrichMessage(service, messageOrMessages) {
571
- return Effect.gen(function* () {
572
- const messages = Array.isArray(messageOrMessages)
573
- ? [...messageOrMessages]
574
- : [messageOrMessages];
575
- const message = messages[0];
576
- const convMeta = service.getConversation(message.conversationId);
577
- const senderNameFor = (agentId) => {
578
- const cachedName = service.getAgentName(agentId);
579
- // `resolveAgentName` has `never` in the error channel — it catches its
580
- // own transport failures and falls back to `senderId` internally, so
581
- // no explicit `catchAll` is needed here.
582
- return cachedName !== undefined
583
- ? Effect.succeed(cachedName)
584
- : service.resolveAgentName(agentId);
585
- };
586
- const senderName = yield* senderNameFor(message.senderId);
587
- const coalesced = [];
588
- for (const [index, m] of messages.entries()) {
589
- const name = index === 0 ? senderName : yield* senderNameFor(m.senderId);
590
- coalesced.push({
591
- id: m.id,
592
- sender: {
593
- id: m.senderId,
594
- name,
595
- },
596
- text: extractTextContent(m.parts),
597
- createdAt: m.createdAt,
598
- ...(m.replyToId ? { replyToId: m.replyToId } : {}),
599
- });
600
- }
601
- const text = coalesced.length === 1
602
- ? coalesced[0].text
603
- : coalesced
604
- .map((m, index) => index === 0
605
- ? m.text
606
- : `[queued message from ${m.sender.name} at ${m.createdAt}]\n${m.text}`)
607
- .join("\n\n");
608
- const isFromMe = service.ownAgentId !== undefined &&
609
- message.senderId === service.ownAgentId;
610
- const conversationMeta = convMeta
611
- ? {
612
- type: convMeta.type === "group" ? "group" : "dm",
613
- name: convMeta.name,
614
- participants: convMeta.participants,
615
- }
616
- : undefined;
617
- const contextBlocks = {};
618
- if (conversationMeta?.type === "group") {
619
- contextBlocks.groupMetadata = conversationMeta;
620
- }
621
- const { entries, commit: commitLegacy } = service.peekContextEntries(message.conversationId);
622
- if (entries.length > 0) {
623
- contextBlocks.crossConversation = entries;
624
- }
625
- const { messages: fullMessages, commit: commitFull } = service.peekFullMessages(message.conversationId);
626
- if (fullMessages.length > 0) {
627
- contextBlocks.crossConversationMessages = fullMessages;
628
- }
629
- const hasContext = entries.length > 0 || fullMessages.length > 0;
630
- return {
631
- enriched: {
632
- id: message.id,
633
- conversationId: message.conversationId,
634
- sender: {
635
- id: message.senderId,
636
- name: senderName,
637
- },
638
- text,
639
- isFromMe,
640
- createdAt: message.createdAt,
641
- replyToId: message.replyToId,
642
- conversationMeta,
643
- contextBlocks,
644
- ...(coalesced.length > 1 ? { coalescedMessages: coalesced } : {}),
645
- },
646
- commitContext: hasContext
647
- ? () => {
648
- commitLegacy();
649
- commitFull();
650
- }
651
- : undefined,
652
- };
653
- });
630
+ return enrichChannelMessage(service, messageOrMessages);
654
631
  }
655
632
  dispatchInboundEffect(messages) {
656
633
  return Effect.gen(this, function* () {