@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.
- package/dist/auth.d.ts +12 -6
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +67 -29
- package/dist/auth.js.map +1 -1
- package/dist/auth.test.js +113 -76
- package/dist/auth.test.js.map +1 -1
- package/dist/channel-base/format-cross-conv.d.ts +43 -0
- package/dist/channel-base/format-cross-conv.d.ts.map +1 -0
- package/dist/channel-base/format-cross-conv.js +69 -0
- package/dist/channel-base/format-cross-conv.js.map +1 -0
- package/dist/channel-base/format-group-block.d.ts +43 -0
- package/dist/channel-base/format-group-block.d.ts.map +1 -0
- package/dist/channel-base/format-group-block.js +59 -0
- package/dist/channel-base/format-group-block.js.map +1 -0
- package/dist/channel-base/index.d.ts +21 -0
- package/dist/channel-base/index.d.ts.map +1 -0
- package/dist/channel-base/index.js +24 -0
- package/dist/channel-base/index.js.map +1 -0
- package/dist/channel-base/lease-guard.d.ts +30 -0
- package/dist/channel-base/lease-guard.d.ts.map +1 -0
- package/dist/channel-base/lease-guard.js +40 -0
- package/dist/channel-base/lease-guard.js.map +1 -0
- package/dist/channel-base/lease-store.d.ts +30 -0
- package/dist/channel-base/lease-store.d.ts.map +1 -0
- package/dist/channel-base/lease-store.js +58 -0
- package/dist/channel-base/lease-store.js.map +1 -0
- package/dist/channel-base/lease.d.ts +94 -0
- package/dist/channel-base/lease.d.ts.map +1 -0
- package/dist/channel-base/lease.js +110 -0
- package/dist/channel-base/lease.js.map +1 -0
- package/dist/channel-core-context.test.d.ts +2 -0
- package/dist/channel-core-context.test.d.ts.map +1 -0
- package/dist/channel-core-context.test.js +334 -0
- package/dist/channel-core-context.test.js.map +1 -0
- package/dist/channel-core-dispatch.test.d.ts +2 -0
- package/dist/channel-core-dispatch.test.d.ts.map +1 -0
- package/dist/channel-core-dispatch.test.js +550 -0
- package/dist/channel-core-dispatch.test.js.map +1 -0
- package/dist/channel-core-enrichment.d.ts +8 -0
- package/dist/channel-core-enrichment.d.ts.map +1 -0
- package/dist/channel-core-enrichment.js +134 -0
- package/dist/channel-core-enrichment.js.map +1 -0
- package/dist/channel-core-errors.d.ts +20 -0
- package/dist/channel-core-errors.d.ts.map +1 -0
- package/dist/channel-core-errors.js +12 -0
- package/dist/channel-core-errors.js.map +1 -0
- package/dist/channel-core-test-support.d.ts +48 -0
- package/dist/channel-core-test-support.d.ts.map +1 -0
- package/dist/channel-core-test-support.js +111 -0
- package/dist/channel-core-test-support.js.map +1 -0
- package/dist/channel-core.d.ts +30 -7
- package/dist/channel-core.d.ts.map +1 -1
- package/dist/channel-core.js +214 -237
- package/dist/channel-core.js.map +1 -1
- package/dist/channel-core.test.js +216 -1082
- package/dist/channel-core.test.js.map +1 -1
- package/dist/cli/__tests__/cli-multi-agent.int.test.js +129 -23
- package/dist/cli/__tests__/cli-multi-agent.int.test.js.map +1 -1
- package/dist/cli/commands/conversations-v2.test.js +64 -54
- package/dist/cli/commands/conversations-v2.test.js.map +1 -1
- package/dist/cli/commands/conversations.d.ts +5 -5
- package/dist/cli/commands/conversations.d.ts.map +1 -1
- package/dist/cli/commands/conversations.js +43 -36
- package/dist/cli/commands/conversations.js.map +1 -1
- package/dist/cli/commands/messages.d.ts +4 -4
- package/dist/cli/commands/messages.d.ts.map +1 -1
- package/dist/cli/commands/messages.js +4 -4
- package/dist/cli/commands/messages.js.map +1 -1
- package/dist/cli/commands/messages.test.js +61 -49
- package/dist/cli/commands/messages.test.js.map +1 -1
- package/dist/cli/commands/ping.d.ts +0 -9
- package/dist/cli/commands/ping.d.ts.map +1 -1
- package/dist/cli/commands/ping.js +1 -1
- package/dist/cli/commands/ping.js.map +1 -1
- package/dist/cli/commands/presence.d.ts +1 -1
- package/dist/cli/commands/presence.js +1 -1
- package/dist/cli/commands/register.d.ts +2 -2
- package/dist/cli/commands/register.d.ts.map +1 -1
- package/dist/cli/commands/register.js +98 -77
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/register.test.js +63 -39
- package/dist/cli/commands/register.test.js.map +1 -1
- package/dist/cli/commands/send.d.ts +6 -6
- package/dist/cli/commands/send.js +6 -6
- package/dist/cli/commands/send.test.js +32 -28
- package/dist/cli/commands/send.test.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +3 -4
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/config.d.ts +0 -2
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +86 -76
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/config.test.js +80 -42
- package/dist/cli/config.test.js.map +1 -1
- package/dist/cli/index.d.ts +1 -16
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +50 -113
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/process-argv.d.ts +2 -0
- package/dist/cli/process-argv.d.ts.map +1 -0
- package/dist/cli/process-argv.js +2 -0
- package/dist/cli/process-argv.js.map +1 -0
- package/dist/cli/profile.d.ts +3 -10
- package/dist/cli/profile.d.ts.map +1 -1
- package/dist/cli/profile.js +105 -104
- package/dist/cli/profile.js.map +1 -1
- package/dist/cli/profile.test.js +338 -232
- package/dist/cli/profile.test.js.map +1 -1
- package/dist/cli/runtime.d.ts +5 -17
- package/dist/cli/runtime.d.ts.map +1 -1
- package/dist/cli/runtime.js +6 -59
- package/dist/cli/runtime.js.map +1 -1
- package/dist/cli/socket-client.d.ts +10 -15
- package/dist/cli/socket-client.d.ts.map +1 -1
- package/dist/cli/socket-client.js +74 -89
- package/dist/cli/socket-client.js.map +1 -1
- package/dist/cli/transport.d.ts +9 -5
- package/dist/cli/transport.d.ts.map +1 -1
- package/dist/cli/transport.js +45 -42
- package/dist/cli/transport.js.map +1 -1
- package/dist/cli/transport.test.js +227 -133
- package/dist/cli/transport.test.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/local-daemon-rpc.d.ts +13 -0
- package/dist/local-daemon-rpc.d.ts.map +1 -0
- package/dist/local-daemon-rpc.js +17 -0
- package/dist/local-daemon-rpc.js.map +1 -0
- package/dist/local-paths.d.ts +7 -0
- package/dist/local-paths.d.ts.map +1 -0
- package/dist/local-paths.js +20 -0
- package/dist/local-paths.js.map +1 -0
- package/dist/runtime/close-info.d.ts +3 -12
- package/dist/runtime/close-info.d.ts.map +1 -1
- package/dist/runtime/close-info.js +28 -39
- package/dist/runtime/close-info.js.map +1 -1
- package/dist/runtime/close-info.test.js +77 -70
- package/dist/runtime/close-info.test.js.map +1 -1
- package/dist/runtime/errors.test.js +42 -28
- package/dist/runtime/errors.test.js.map +1 -1
- package/dist/runtime/frame.d.ts +4 -31
- package/dist/runtime/frame.d.ts.map +1 -1
- package/dist/runtime/frame.js +7 -116
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/frame.test.js +41 -54
- package/dist/runtime/frame.test.js.map +1 -1
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +7 -11
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/local-history.d.ts +67 -0
- package/dist/runtime/local-history.d.ts.map +1 -0
- package/dist/runtime/local-history.js +77 -0
- package/dist/runtime/local-history.js.map +1 -0
- package/dist/runtime/local-service-commands.d.ts +19 -0
- package/dist/runtime/local-service-commands.d.ts.map +1 -1
- package/dist/runtime/local-service-commands.js +23 -0
- package/dist/runtime/local-service-commands.js.map +1 -1
- package/dist/runtime/local-socket-server.d.ts +20 -0
- package/dist/runtime/local-socket-server.d.ts.map +1 -0
- package/dist/runtime/local-socket-server.js +94 -0
- package/dist/runtime/local-socket-server.js.map +1 -0
- package/dist/runtime/refs.d.ts +1 -1
- package/dist/runtime/refs.js +1 -1
- package/dist/runtime/runtime.test.js +29 -21
- package/dist/runtime/runtime.test.js.map +1 -1
- package/dist/runtime/service-helpers.d.ts +32 -0
- package/dist/runtime/service-helpers.d.ts.map +1 -0
- package/dist/runtime/service-helpers.js +85 -0
- package/dist/runtime/service-helpers.js.map +1 -0
- package/dist/runtime/subscribers.d.ts +13 -17
- package/dist/runtime/subscribers.d.ts.map +1 -1
- package/dist/runtime/subscribers.js +37 -32
- package/dist/runtime/subscribers.js.map +1 -1
- package/dist/runtime/subscribers.test.js +139 -131
- package/dist/runtime/subscribers.test.js.map +1 -1
- package/dist/service-socket-path.test.d.ts +2 -0
- package/dist/service-socket-path.test.d.ts.map +1 -0
- package/dist/service-socket-path.test.js +87 -0
- package/dist/service-socket-path.test.js.map +1 -0
- package/dist/service.d.ts +23 -20
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +192 -340
- package/dist/service.js.map +1 -1
- package/dist/service.test.js +652 -653
- package/dist/service.test.js.map +1 -1
- package/dist/test/index.d.ts +30 -14
- package/dist/test/index.d.ts.map +1 -1
- package/dist/test/index.js +25 -21
- package/dist/test/index.js.map +1 -1
- package/dist/test-utils/channel-service-fixture.d.ts +1 -0
- package/dist/test-utils/channel-service-fixture.d.ts.map +1 -1
- package/dist/test-utils/channel-service-fixture.js +139 -98
- package/dist/test-utils/channel-service-fixture.js.map +1 -1
- package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
- package/dist/test-utils/conformance-adapter.js +145 -124
- package/dist/test-utils/conformance-adapter.js.map +1 -1
- package/dist/test-utils/fake-service.d.ts.map +1 -1
- package/dist/test-utils/fake-service.js +3 -3
- package/dist/test-utils/fake-service.js.map +1 -1
- package/dist/test-utils/index.d.ts +25 -0
- package/dist/test-utils/index.d.ts.map +1 -1
- package/dist/test-utils/index.js +25 -8
- package/dist/test-utils/index.js.map +1 -1
- package/dist/ws-client-test-support.d.ts +180 -0
- package/dist/ws-client-test-support.d.ts.map +1 -0
- package/dist/ws-client-test-support.js +399 -0
- package/dist/ws-client-test-support.js.map +1 -0
- package/dist/ws-client.d.ts +59 -32
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +272 -321
- package/dist/ws-client.js.map +1 -1
- package/dist/ws-client.test.js +388 -800
- package/dist/ws-client.test.js.map +1 -1
- package/package.json +18 -10
package/dist/channel-core.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
/**
|
|
86
|
-
*
|
|
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
|
-
|
|
96
|
+
runBackgroundLog(effectLogInfo("MoltZapChannelCore: dropping inbound message for closed conversation", {
|
|
99
97
|
messageId: message.id,
|
|
100
98
|
conversationId: message.conversationId,
|
|
101
|
-
}
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
}
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
261
|
+
runBackgroundLog(effectLogInfo("MoltZapChannelCore: closed conversation dispatch work purged", {
|
|
254
262
|
conversationId,
|
|
255
263
|
droppedQueued,
|
|
256
|
-
}
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
...(
|
|
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*
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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* () {
|