@moltzap/client 2026.511.1 → 2026.518.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-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 +14 -10
package/dist/service.js
CHANGED
|
@@ -1,50 +1,39 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import * as os from "node:os";
|
|
1
|
+
import { FileSystem, Path } from "@effect/platform";
|
|
2
|
+
import { NodeContext } from "@effect/platform-node";
|
|
5
3
|
import { AgentsLookup, AgentsLookupByName, ConversationsCreate, ConversationsGet, DispatchRequest, DispatchRelease, DispatchesConsumed, DispatchesExpired, DispatchesGet, MessageReceivedNotificationDefinition, ConversationCreatedNotificationDefinition, ConversationUpdatedNotificationDefinition, ConversationArchivedNotificationDefinition, ConversationUnarchivedNotificationDefinition, ConversationArchivedError, MessagesList, MessagesSend, NotConnectedError, rpcMethods, RpcServerError, RpcTimeoutError, } from "@moltzap/protocol";
|
|
6
|
-
import { Config, ConfigProvider,
|
|
7
|
-
import { MoltZapWsClient
|
|
4
|
+
import { Config, ConfigProvider, Effect, HashMap, Option, Ref, Scope, } from "effect";
|
|
5
|
+
import { MoltZapWsClient } from "./ws-client.js";
|
|
8
6
|
import { AgentNotFoundError } from "./runtime/errors.js";
|
|
9
7
|
import { getOr, snapshot } from "./runtime/refs.js";
|
|
10
8
|
import { LocalServiceCommands } from "./runtime/local-service-commands.js";
|
|
11
|
-
|
|
9
|
+
import { getMoltZapAgentServiceSocketPath, getMoltZapServiceSocketPath, } from "./local-paths.js";
|
|
10
|
+
import { startLocalSocketServer, stopLocalSocketServer, } from "./runtime/local-socket-server.js";
|
|
11
|
+
import { buildContextEntries, makeContextCandidate, newMessagesForConversation, notificationTraceRecord, renderPart, ServiceInputError, } from "./runtime/service-helpers.js";
|
|
12
|
+
import { decodeHistoryRequest, formatHistoryMessage, lastReadIdsForSession, } from "./runtime/local-history.js";
|
|
12
13
|
const CROSS_CONTEXT_TEXT_LIMIT = 120;
|
|
13
|
-
const DEFAULT_HISTORY_LIMIT = 10;
|
|
14
14
|
const DEFAULT_MAX_CONTEXT_CONVERSATIONS = 5;
|
|
15
15
|
const DEFAULT_MAX_MESSAGES_PER_CONVERSATION = 3;
|
|
16
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
17
|
const ClientEventLogDir = Config.option(Config.string("MOLTZAP_CLIENT_EVENT_LOG_DIR"));
|
|
22
18
|
const getClientEventLogDir = () => Option.getOrUndefined(Effect.runSync(ClientEventLogDir.pipe(Effect.withConfigProvider(ConfigProvider.fromEnv()))));
|
|
23
19
|
function appendClientEventTrace(record) {
|
|
24
20
|
const dir = getClientEventLogDir();
|
|
25
21
|
if (!dir)
|
|
26
|
-
return;
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
return Effect.void;
|
|
23
|
+
return Effect.gen(function* () {
|
|
24
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
25
|
+
const path = yield* Path.Path;
|
|
29
26
|
const agentId = typeof record["agentId"] === "string" ? record["agentId"] : "unknown";
|
|
30
27
|
const safeAgentId = /^[A-Za-z0-9_-]+$/.test(agentId) ? agentId : "unknown";
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
catch (err) {
|
|
34
|
-
try {
|
|
35
|
-
process.stderr.write(`moltzap client event trace write failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
36
|
-
}
|
|
37
|
-
catch (stderrErr) {
|
|
38
|
-
void stderrErr;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
28
|
+
yield* Effect.zipRight(fileSystem.makeDirectory(dir, { recursive: true }), fileSystem.writeFileString(path.join(dir, `client-events-${safeAgentId}.jsonl`), JSON.stringify(record) + "\n", { flag: "a" }));
|
|
29
|
+
}).pipe(Effect.withSpan("appendClientEventTrace"), Effect.provide(NodeContext.layer), Effect.catchAll((err) => Effect.logWarning("moltzap client event trace write failed", err)));
|
|
41
30
|
}
|
|
42
|
-
/** Escape
|
|
31
|
+
/** Escape `<`, `>`, `&` so sender content can't escape a `<system-reminder>` block. */
|
|
43
32
|
export function sanitizeForSystemReminder(s) {
|
|
44
33
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
45
34
|
}
|
|
46
35
|
/**
|
|
47
|
-
* Format CrossConversationEntry[] as a
|
|
36
|
+
* Format CrossConversationEntry[] as a `<system-reminder>` block. Adapters
|
|
48
37
|
* that inline context into prompt text (nanoclaw) and `MoltZapService.getContext`
|
|
49
38
|
* share this formatter so sanitization and line shape stay in one place.
|
|
50
39
|
*/
|
|
@@ -63,11 +52,6 @@ export function formatCrossConversationBlock(entries, opts) {
|
|
|
63
52
|
"</system-reminder>",
|
|
64
53
|
].join("\n");
|
|
65
54
|
}
|
|
66
|
-
const renderPart = Match.type().pipe(Match.discriminatorsExhaustive("type")({
|
|
67
|
-
text: (t) => t.text,
|
|
68
|
-
image: () => "[image]",
|
|
69
|
-
file: (f) => `[file: ${f.name}]`,
|
|
70
|
-
}));
|
|
71
55
|
/**
|
|
72
56
|
* Per-conversation message cap. Older messages are evicted FIFO; the
|
|
73
57
|
* on-disk history remains the source of truth. Sized for typical CLI
|
|
@@ -83,15 +67,15 @@ const MAX_MESSAGES_PER_CONV = 1000;
|
|
|
83
67
|
const DEDUP_WINDOW_PER_CONV = 1000;
|
|
84
68
|
/**
|
|
85
69
|
* Invoke every handler with `arg`, isolating throws so one bad handler
|
|
86
|
-
* doesn't abort the remaining fanout.
|
|
70
|
+
* doesn't abort the remaining fanout.
|
|
87
71
|
*/
|
|
88
|
-
function fanout(handlers, arg
|
|
72
|
+
function fanout(handlers, arg) {
|
|
89
73
|
for (const h of handlers) {
|
|
90
74
|
try {
|
|
91
75
|
h(arg);
|
|
92
76
|
}
|
|
93
77
|
catch (err) {
|
|
94
|
-
|
|
78
|
+
Effect.runFork(Effect.logError("notification handler threw", err));
|
|
95
79
|
}
|
|
96
80
|
}
|
|
97
81
|
}
|
|
@@ -138,6 +122,40 @@ export class MoltZapService {
|
|
|
138
122
|
dispatchesConsumed: [],
|
|
139
123
|
dispatchesExpired: [],
|
|
140
124
|
};
|
|
125
|
+
notificationDispatchers = new Map([
|
|
126
|
+
[
|
|
127
|
+
MessageReceivedNotificationDefinition,
|
|
128
|
+
(notification) => this.handleMessageReceivedNotification(notification.params),
|
|
129
|
+
],
|
|
130
|
+
[
|
|
131
|
+
ConversationCreatedNotificationDefinition,
|
|
132
|
+
(notification) => this.handleConversationCreatedNotification(notification.params),
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
ConversationUpdatedNotificationDefinition,
|
|
136
|
+
(notification) => this.handleConversationUpdatedNotification(notification.params),
|
|
137
|
+
],
|
|
138
|
+
[
|
|
139
|
+
ConversationArchivedNotificationDefinition,
|
|
140
|
+
(notification) => this.handleConversationArchivedNotification(notification.params),
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
ConversationUnarchivedNotificationDefinition,
|
|
144
|
+
(notification) => this.handleConversationUnarchivedNotification(notification.params),
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
DispatchRelease,
|
|
148
|
+
(notification) => fanout(this.handlers.dispatchRelease, notification.params),
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
DispatchesConsumed,
|
|
152
|
+
(notification) => fanout(this.handlers.dispatchesConsumed, notification.params),
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
DispatchesExpired,
|
|
156
|
+
(notification) => fanout(this.handlers.dispatchesExpired, notification.params),
|
|
157
|
+
],
|
|
158
|
+
]);
|
|
141
159
|
_ownAgentId;
|
|
142
160
|
constructor(opts) {
|
|
143
161
|
this.opts = opts;
|
|
@@ -154,18 +172,16 @@ export class MoltZapService {
|
|
|
154
172
|
this.client = new MoltZapWsClient({
|
|
155
173
|
serverUrl: this.opts.serverUrl,
|
|
156
174
|
agentKey: this.opts.agentKey,
|
|
157
|
-
logger: this.opts.logger,
|
|
158
175
|
// Spec #222 OQ-6: arg required. The body doesn't branch on
|
|
159
176
|
// close metadata today; signature kept explicit so a future
|
|
160
177
|
// disconnect-handler chain can plumb code/reason through.
|
|
161
|
-
onDisconnect: (
|
|
162
|
-
void close;
|
|
178
|
+
onDisconnect: () => {
|
|
163
179
|
this._connected = false;
|
|
164
|
-
fanout(this.handlers.disconnect, undefined
|
|
180
|
+
fanout(this.handlers.disconnect, undefined);
|
|
165
181
|
},
|
|
166
182
|
onReconnect: (helloOk) => {
|
|
167
183
|
this._connected = true;
|
|
168
|
-
fanout(this.handlers.reconnect, helloOk
|
|
184
|
+
fanout(this.handlers.reconnect, helloOk);
|
|
169
185
|
},
|
|
170
186
|
});
|
|
171
187
|
// Spec #222 OQ-4: per-notification callback is replaced by subscriptions.
|
|
@@ -182,15 +198,15 @@ export class MoltZapService {
|
|
|
182
198
|
}
|
|
183
199
|
/**
|
|
184
200
|
* Tear down the service. `close()` is sync because it fans out to the
|
|
185
|
-
* socket server, Refs, and the ws-client
|
|
186
|
-
*
|
|
187
|
-
* shutdown
|
|
201
|
+
* socket server, Refs, and the ws-client. Effectful network/filesystem
|
|
202
|
+
* cleanup is forked at the edge so existing callers still get immediate
|
|
203
|
+
* shutdown.
|
|
188
204
|
*/
|
|
189
205
|
close() {
|
|
190
206
|
this._connected = false;
|
|
191
|
-
this.stopSocketServer();
|
|
207
|
+
Effect.runFork(this.stopSocketServer());
|
|
192
208
|
if (this.client) {
|
|
193
|
-
|
|
209
|
+
Effect.runFork(this.client.close());
|
|
194
210
|
}
|
|
195
211
|
this.client = null;
|
|
196
212
|
Effect.runSync(Effect.all([
|
|
@@ -209,10 +225,10 @@ export class MoltZapService {
|
|
|
209
225
|
// service instance.
|
|
210
226
|
}
|
|
211
227
|
// --- Socket Server ---
|
|
212
|
-
|
|
228
|
+
socketServerScope = null;
|
|
213
229
|
activeSocketPath = null;
|
|
214
230
|
/** Default socket path for CLI discovery. Per-instance path uses agentId. */
|
|
215
|
-
static SOCKET_PATH =
|
|
231
|
+
static SOCKET_PATH = getMoltZapServiceSocketPath();
|
|
216
232
|
/**
|
|
217
233
|
* `agentId` is a server-assigned string. Treat it as untrusted: if a
|
|
218
234
|
* compromised or malicious server returns an id containing `..` or a
|
|
@@ -225,127 +241,39 @@ export class MoltZapService {
|
|
|
225
241
|
/** Per-instance socket path based on connected agentId. */
|
|
226
242
|
get socketPath() {
|
|
227
243
|
const id = MoltZapService.safeAgentIdSegment(this.ownAgentId ?? "default");
|
|
228
|
-
return
|
|
244
|
+
return getMoltZapAgentServiceSocketPath(id);
|
|
229
245
|
}
|
|
230
246
|
startSocketServer() {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// ENOENT is the normal case (no stale socket); log everything
|
|
238
|
-
// else (permission denied, EACCES) so operators can diagnose.
|
|
239
|
-
const code = err.code;
|
|
240
|
-
if (code !== "ENOENT") {
|
|
241
|
-
this.opts.logger?.warn("unlink existing socket failed", err);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
fs.mkdirSync(path.dirname(sockPath), { recursive: true });
|
|
245
|
-
this.socketServer = net.createServer((conn) => {
|
|
246
|
-
let buffer = "";
|
|
247
|
-
conn.on("data", (chunk) => {
|
|
248
|
-
buffer += chunk.toString();
|
|
249
|
-
const lines = buffer.split("\n");
|
|
250
|
-
buffer = lines.pop();
|
|
251
|
-
for (const line of lines) {
|
|
252
|
-
if (!line.trim())
|
|
253
|
-
continue;
|
|
254
|
-
// Fork the line-handler Effect onto the Effect runtime. Each line
|
|
255
|
-
// dispatches independently; the fiber runs until it writes a
|
|
256
|
-
// response or error frame back onto the socket.
|
|
257
|
-
Effect.runFork(this.handleSocketLineEffect(line, conn));
|
|
258
|
-
}
|
|
247
|
+
return Effect.gen(this, function* () {
|
|
248
|
+
const previous = this.resetSocketServerState();
|
|
249
|
+
yield* stopLocalSocketServer({
|
|
250
|
+
socketScope: previous.socketScope,
|
|
251
|
+
socketPath: previous.sockPath,
|
|
252
|
+
defaultSocketPath: MoltZapService.SOCKET_PATH,
|
|
259
253
|
});
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
catch (err) {
|
|
269
|
-
this.opts.logger?.warn("chmod 0600 on socket failed", err);
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
// Symlink default path to this instance for CLI discovery
|
|
273
|
-
try {
|
|
274
|
-
fs.unlinkSync(MoltZapService.SOCKET_PATH);
|
|
275
|
-
}
|
|
276
|
-
catch (err) {
|
|
277
|
-
// Stale/missing default symlink is expected most of the time; log
|
|
278
|
-
// at debug so real permission errors still surface.
|
|
279
|
-
this.opts.logger?.info("unlink default socket symlink", err);
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
fs.symlinkSync(sockPath, MoltZapService.SOCKET_PATH);
|
|
283
|
-
}
|
|
284
|
-
catch (err) {
|
|
285
|
-
this.opts.logger?.warn("symlink default socket failed", err);
|
|
286
|
-
}
|
|
254
|
+
const running = yield* startLocalSocketServer({
|
|
255
|
+
socketPath: this.socketPath,
|
|
256
|
+
defaultSocketPath: MoltZapService.SOCKET_PATH,
|
|
257
|
+
handleRequest: (method, params) => this.handleSocketRequestEffect(method, params),
|
|
258
|
+
});
|
|
259
|
+
this.socketServerScope = running.socketScope;
|
|
260
|
+
this.activeSocketPath = running.socketPath;
|
|
261
|
+
}).pipe(Effect.withSpan("MoltZapService.startSocketServer"));
|
|
287
262
|
}
|
|
288
|
-
|
|
289
|
-
this.
|
|
290
|
-
this.
|
|
263
|
+
resetSocketServerState() {
|
|
264
|
+
const socketScope = this.socketServerScope;
|
|
265
|
+
this.socketServerScope = null;
|
|
291
266
|
const sockPath = this.activeSocketPath ?? this.socketPath;
|
|
292
267
|
this.activeSocketPath = null;
|
|
293
|
-
|
|
294
|
-
fs.unlinkSync(sockPath);
|
|
295
|
-
}
|
|
296
|
-
catch (err) {
|
|
297
|
-
this.opts.logger?.info("unlink socket path", err);
|
|
298
|
-
}
|
|
299
|
-
// Remove default symlink only if it points to this instance.
|
|
300
|
-
try {
|
|
301
|
-
const target = fs.readlinkSync(MoltZapService.SOCKET_PATH);
|
|
302
|
-
if (target === sockPath)
|
|
303
|
-
fs.unlinkSync(MoltZapService.SOCKET_PATH);
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
this.opts.logger?.info("cleanup default symlink", err);
|
|
307
|
-
}
|
|
268
|
+
return { socketScope, sockPath };
|
|
308
269
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return Effect.suspend(() => {
|
|
317
|
-
let req;
|
|
318
|
-
try {
|
|
319
|
-
const parsed = JSON.parse(line);
|
|
320
|
-
if (!isPlainRecord(parsed)) {
|
|
321
|
-
return Effect.sync(() => conn.write(JSON.stringify({
|
|
322
|
-
error: "request must be a JSON object",
|
|
323
|
-
}) + "\n"));
|
|
324
|
-
}
|
|
325
|
-
req = parsed;
|
|
326
|
-
}
|
|
327
|
-
catch (err) {
|
|
328
|
-
return Effect.sync(() => conn.write(JSON.stringify({
|
|
329
|
-
error: err instanceof Error ? err.message : String(err),
|
|
330
|
-
}) + "\n"));
|
|
331
|
-
}
|
|
332
|
-
if (typeof req.method !== "string" || !req.method) {
|
|
333
|
-
return Effect.sync(() => conn.write(JSON.stringify({
|
|
334
|
-
error: "method is required and must be a string",
|
|
335
|
-
}) + "\n"));
|
|
336
|
-
}
|
|
337
|
-
const params = isPlainRecord(req.params) ? req.params : {};
|
|
338
|
-
return this.handleSocketRequestEffect(req.method, params).pipe(Effect.match({
|
|
339
|
-
onSuccess: (result) => {
|
|
340
|
-
conn.write(JSON.stringify({ result }) + "\n");
|
|
341
|
-
},
|
|
342
|
-
onFailure: (err) => {
|
|
343
|
-
conn.write(JSON.stringify({
|
|
344
|
-
error: err instanceof Error ? err.message : String(err),
|
|
345
|
-
}) + "\n");
|
|
346
|
-
},
|
|
347
|
-
}));
|
|
348
|
-
});
|
|
270
|
+
stopSocketServer() {
|
|
271
|
+
const { socketScope, sockPath } = this.resetSocketServerState();
|
|
272
|
+
return stopLocalSocketServer({
|
|
273
|
+
socketScope,
|
|
274
|
+
socketPath: sockPath,
|
|
275
|
+
defaultSocketPath: MoltZapService.SOCKET_PATH,
|
|
276
|
+
}).pipe(Effect.withSpan("MoltZapService.stopSocketServer"));
|
|
349
277
|
}
|
|
350
278
|
handleSocketRequestEffect(method, params) {
|
|
351
279
|
return Effect.suspend(() => {
|
|
@@ -362,7 +290,8 @@ export class MoltZapService {
|
|
|
362
290
|
return this.handleHistoryRequest(params);
|
|
363
291
|
default: {
|
|
364
292
|
const definition = rpcMethods.find((d) => d.name === method);
|
|
365
|
-
if (definition === undefined ||
|
|
293
|
+
if (definition === undefined ||
|
|
294
|
+
!definition.validateParams(params)) {
|
|
366
295
|
return Effect.fail(new ServiceInputError({
|
|
367
296
|
message: `unknown or invalid RPC method: ${method}`,
|
|
368
297
|
}));
|
|
@@ -374,85 +303,80 @@ export class MoltZapService {
|
|
|
374
303
|
}
|
|
375
304
|
handleHistoryRequest(params) {
|
|
376
305
|
return Effect.gen(this, function* () {
|
|
377
|
-
|
|
378
|
-
return yield* Effect.fail(new ServiceInputError({
|
|
379
|
-
message: "conversationId is required and must be a string",
|
|
380
|
-
}));
|
|
381
|
-
}
|
|
382
|
-
if (params.limit !== undefined && typeof params.limit !== "number") {
|
|
383
|
-
return yield* Effect.fail(new ServiceInputError({ message: "limit must be a number" }));
|
|
384
|
-
}
|
|
385
|
-
const convId = params.conversationId;
|
|
386
|
-
const limit = params.limit ?? DEFAULT_HISTORY_LIMIT;
|
|
387
|
-
const sessionKey = params.sessionKey;
|
|
306
|
+
const request = yield* decodeHistoryRequest(params);
|
|
388
307
|
const result = yield* this.sendRpc(MessagesList, {
|
|
389
|
-
conversationId:
|
|
390
|
-
limit,
|
|
308
|
+
conversationId: request.conversationId,
|
|
309
|
+
limit: request.limit,
|
|
391
310
|
});
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
const unknownAgentIds = [
|
|
395
|
-
...new Set(result.messages.map((m) => m.senderId)),
|
|
396
|
-
].filter((id) => !HashMap.has(knownNames, id));
|
|
397
|
-
const lookupEff = unknownAgentIds.length > 0
|
|
398
|
-
? this.sendRpc(AgentsLookup, { agentIds: unknownAgentIds }).pipe(Effect.tap((res) => {
|
|
399
|
-
return Ref.update(this.agentNamesRef, (names) => {
|
|
400
|
-
let next = names;
|
|
401
|
-
for (const a of res.agents) {
|
|
402
|
-
next = HashMap.set(next, a.id, a.name);
|
|
403
|
-
}
|
|
404
|
-
return next;
|
|
405
|
-
});
|
|
406
|
-
}), Effect.asVoid, Effect.catchAll(() => Effect.void))
|
|
407
|
-
: Effect.void;
|
|
408
|
-
const metaEff = this.sendRpc(ConversationsGet, {
|
|
409
|
-
conversationId: convId,
|
|
410
|
-
}).pipe(Effect.map((res) => res.conversation), Effect.catchAll(() => Effect.succeed(undefined)));
|
|
411
|
-
const [, convMeta] = yield* Effect.all([lookupEff, metaEff], {
|
|
412
|
-
concurrency: HISTORY_LOOKUP_CONCURRENCY,
|
|
413
|
-
});
|
|
414
|
-
// Determine what's "new" using lastRead (not lastNotified).
|
|
415
|
-
// lastNotified is advanced by getContext() (system-reminder).
|
|
416
|
-
// lastRead is advanced here when the agent explicitly reads history.
|
|
417
|
-
const allAgentNames = yield* Ref.get(this.agentNamesRef);
|
|
311
|
+
const convMeta = yield* this.loadHistorySupportData(request.conversationId, result.messages);
|
|
312
|
+
const agentNames = yield* Ref.get(this.agentNamesRef);
|
|
418
313
|
const lastReadMap = yield* Ref.get(this.lastReadRef);
|
|
419
|
-
const lastReadIds =
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
senderId: m.senderId,
|
|
428
|
-
senderName: m.senderId === this.ownAgentId ? "you" : senderName,
|
|
429
|
-
isOwn: m.senderId === this.ownAgentId,
|
|
430
|
-
text,
|
|
431
|
-
createdAt: m.createdAt,
|
|
432
|
-
isNew: sessionKey ? !lastReadIds.has(m.id) : false,
|
|
433
|
-
};
|
|
434
|
-
});
|
|
435
|
-
// Advance lastRead to include all message IDs in the returned page.
|
|
436
|
-
if (sessionKey && result.messages.length > 0) {
|
|
437
|
-
yield* Ref.update(this.lastReadRef, (outer) => {
|
|
438
|
-
const perSession = getOr(outer, sessionKey, () => HashMap.empty());
|
|
439
|
-
const existing = getOr(perSession, convId, () => new Set());
|
|
440
|
-
if (result.messages.every((m) => existing.has(m.id)))
|
|
441
|
-
return outer;
|
|
442
|
-
const nextSet = new Set(existing);
|
|
443
|
-
for (const m of result.messages)
|
|
444
|
-
nextSet.add(m.id);
|
|
445
|
-
return HashMap.set(outer, sessionKey, HashMap.set(perSession, convId, nextSet));
|
|
446
|
-
});
|
|
447
|
-
}
|
|
314
|
+
const lastReadIds = lastReadIdsForSession(lastReadMap, request);
|
|
315
|
+
const messages = result.messages.map((message) => formatHistoryMessage(message, {
|
|
316
|
+
agentNames,
|
|
317
|
+
ownAgentId: this.ownAgentId,
|
|
318
|
+
lastReadIds,
|
|
319
|
+
hasSessionKey: request.sessionKey !== undefined,
|
|
320
|
+
}));
|
|
321
|
+
yield* this.advanceHistoryLastRead(request, result.messages);
|
|
448
322
|
return {
|
|
449
323
|
messages,
|
|
450
324
|
hasMore: result.hasMore,
|
|
451
325
|
conversationMeta: convMeta,
|
|
452
|
-
newCount: messages.filter((
|
|
326
|
+
newCount: messages.filter((message) => message.isNew).length,
|
|
453
327
|
};
|
|
454
328
|
});
|
|
455
329
|
}
|
|
330
|
+
loadHistorySupportData(convId, messages) {
|
|
331
|
+
return Effect.gen(this, function* () {
|
|
332
|
+
const [, convMeta] = yield* Effect.all([
|
|
333
|
+
this.refreshHistoryAgentNames(messages),
|
|
334
|
+
this.fetchHistoryConversationMeta(convId),
|
|
335
|
+
], {
|
|
336
|
+
concurrency: HISTORY_LOOKUP_CONCURRENCY,
|
|
337
|
+
});
|
|
338
|
+
return convMeta;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
refreshHistoryAgentNames(messages) {
|
|
342
|
+
return Effect.gen(this, function* () {
|
|
343
|
+
const knownNames = yield* Ref.get(this.agentNamesRef);
|
|
344
|
+
const unknownAgentIds = [
|
|
345
|
+
...new Set(messages.map((message) => message.senderId)),
|
|
346
|
+
].filter((id) => !HashMap.has(knownNames, id));
|
|
347
|
+
if (unknownAgentIds.length === 0)
|
|
348
|
+
return;
|
|
349
|
+
yield* this.sendRpc(AgentsLookup, { agentIds: unknownAgentIds }).pipe(Effect.tap((result) => Ref.update(this.agentNamesRef, (names) => {
|
|
350
|
+
let next = names;
|
|
351
|
+
for (const agent of result.agents) {
|
|
352
|
+
next = HashMap.set(next, agent.id, agent.name);
|
|
353
|
+
}
|
|
354
|
+
return next;
|
|
355
|
+
})), Effect.asVoid, Effect.catchAll(() => Effect.void));
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
fetchHistoryConversationMeta(convId) {
|
|
359
|
+
return this.sendRpc(ConversationsGet, {
|
|
360
|
+
conversationId: convId,
|
|
361
|
+
}).pipe(Effect.map((result) => result.conversation), Effect.catchAll(() => Effect.succeed(undefined)));
|
|
362
|
+
}
|
|
363
|
+
advanceHistoryLastRead(request, messages) {
|
|
364
|
+
if (request.sessionKey === undefined || messages.length === 0) {
|
|
365
|
+
return Effect.void;
|
|
366
|
+
}
|
|
367
|
+
const { conversationId, sessionKey } = request;
|
|
368
|
+
return Ref.update(this.lastReadRef, (outer) => {
|
|
369
|
+
const perSession = getOr(outer, sessionKey, () => HashMap.empty());
|
|
370
|
+
const existing = getOr(perSession, conversationId, () => new Set());
|
|
371
|
+
if (messages.every((message) => existing.has(message.id)))
|
|
372
|
+
return outer;
|
|
373
|
+
const nextSet = new Set(existing);
|
|
374
|
+
for (const message of messages) {
|
|
375
|
+
nextSet.add(message.id);
|
|
376
|
+
}
|
|
377
|
+
return HashMap.set(outer, sessionKey, HashMap.set(perSession, conversationId, nextSet));
|
|
378
|
+
});
|
|
379
|
+
}
|
|
456
380
|
// --- Conversations ---
|
|
457
381
|
getConversation(convId) {
|
|
458
382
|
return Option.getOrUndefined(HashMap.get(snapshot(this.conversationsRef), convId));
|
|
@@ -556,7 +480,8 @@ export class MoltZapService {
|
|
|
556
480
|
* per viewing conversation and advanced after notification.
|
|
557
481
|
*/
|
|
558
482
|
getContext(currentConvId, opts) {
|
|
559
|
-
const
|
|
483
|
+
const contextOptions = opts ?? {};
|
|
484
|
+
const { entries, commit } = this.peekContextEntries(currentConvId, contextOptions);
|
|
560
485
|
if (entries.length === 0)
|
|
561
486
|
return null;
|
|
562
487
|
commit();
|
|
@@ -573,40 +498,9 @@ export class MoltZapService {
|
|
|
573
498
|
peekContextEntries(currentConvId, opts) {
|
|
574
499
|
const maxConvs = opts?.maxConversations ?? DEFAULT_MAX_CONTEXT_CONVERSATIONS;
|
|
575
500
|
const maxMsgsPerConv = opts?.maxMessagesPerConv ?? DEFAULT_MAX_MESSAGES_PER_CONVERSATION;
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
// order is hash-based (not insertion/recency), so without the sort the
|
|
580
|
-
// maxConvs truncation would pick an arbitrary subset and could omit the
|
|
581
|
-
// freshest conversations.
|
|
582
|
-
const candidates = [];
|
|
583
|
-
for (const [convId, newMsgs] of this.iterNewMessagesByConv(messagesMap, viewMarkers, currentConvId)) {
|
|
584
|
-
const last = newMsgs[newMsgs.length - 1];
|
|
585
|
-
candidates.push({
|
|
586
|
-
convId,
|
|
587
|
-
newMsgs,
|
|
588
|
-
lastTs: new Date(last.createdAt).getTime(),
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
candidates.sort((a, b) => b.lastTs - a.lastTs);
|
|
592
|
-
const entries = [];
|
|
593
|
-
const pendingAdvances = [];
|
|
594
|
-
for (const { convId, newMsgs } of candidates.slice(0, maxConvs)) {
|
|
595
|
-
const reportable = newMsgs.slice(-maxMsgsPerConv);
|
|
596
|
-
const last = reportable[reportable.length - 1];
|
|
597
|
-
const senderName = getOr(agentNamesMap, last.senderId, () => last.senderId);
|
|
598
|
-
const minutesAgo = Math.max(0, Math.round((Date.now() - new Date(last.createdAt).getTime()) /
|
|
599
|
-
MILLISECONDS_PER_MINUTE));
|
|
600
|
-
entries.push({
|
|
601
|
-
conversationId: convId,
|
|
602
|
-
conversationName: Option.getOrUndefined(HashMap.get(conversationsMap, convId))?.name,
|
|
603
|
-
senderName,
|
|
604
|
-
text: last.parts.map(renderPart).join(" "),
|
|
605
|
-
minutesAgo,
|
|
606
|
-
count: reportable.length,
|
|
607
|
-
});
|
|
608
|
-
pendingAdvances.push([convId, last.id]);
|
|
609
|
-
}
|
|
501
|
+
const state = this.readCrossConvState(currentConvId);
|
|
502
|
+
const candidates = this.collectContextCandidates(state, currentConvId);
|
|
503
|
+
const { entries, pendingAdvances } = buildContextEntries(candidates.slice(0, maxConvs), state, maxMsgsPerConv);
|
|
610
504
|
return {
|
|
611
505
|
entries,
|
|
612
506
|
commit: () => this.advanceLastNotified(currentConvId, pendingAdvances),
|
|
@@ -650,18 +544,19 @@ export class MoltZapService {
|
|
|
650
544
|
viewMarkers: getOr(lastNotifiedMap, currentConvId, () => HashMap.empty()),
|
|
651
545
|
};
|
|
652
546
|
}
|
|
547
|
+
collectContextCandidates(state, currentConvId) {
|
|
548
|
+
const candidates = [];
|
|
549
|
+
for (const [convId, newMsgs] of this.iterNewMessagesByConv(state.messagesMap, state.viewMarkers, currentConvId)) {
|
|
550
|
+
candidates.push(makeContextCandidate(convId, newMsgs));
|
|
551
|
+
}
|
|
552
|
+
candidates.sort((a, b) => b.lastTs - a.lastTs);
|
|
553
|
+
return candidates;
|
|
554
|
+
}
|
|
653
555
|
*iterNewMessagesByConv(messagesMap, viewMarkers, currentConvId) {
|
|
654
556
|
for (const [convId, msgs] of messagesMap) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
const lastSeenIdx = lastSeenId
|
|
659
|
-
? msgs.findIndex((m) => m.id === lastSeenId)
|
|
660
|
-
: -1;
|
|
661
|
-
const newMsgs = msgs.slice(lastSeenIdx + 1);
|
|
662
|
-
if (newMsgs.length === 0)
|
|
663
|
-
continue;
|
|
664
|
-
yield [convId, newMsgs];
|
|
557
|
+
const newMsgs = newMessagesForConversation(convId, msgs, viewMarkers, currentConvId);
|
|
558
|
+
if (newMsgs.length > 0)
|
|
559
|
+
yield [convId, newMsgs];
|
|
665
560
|
}
|
|
666
561
|
}
|
|
667
562
|
advanceLastNotified(currentConvId, pendingAdvances) {
|
|
@@ -716,70 +611,19 @@ export class MoltZapService {
|
|
|
716
611
|
Effect.catchAll(() => Effect.void));
|
|
717
612
|
}
|
|
718
613
|
handleNotification(notification) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
: undefined;
|
|
731
|
-
appendClientEventTrace({
|
|
732
|
-
ts: new Date().toISOString(),
|
|
733
|
-
agentId: this._ownAgentId ?? "unknown",
|
|
734
|
-
notification: notification.method,
|
|
735
|
-
messageId: message?.["id"],
|
|
736
|
-
messageConversationId: message?.["conversationId"],
|
|
737
|
-
messageSenderId: message?.["senderId"],
|
|
738
|
-
conversationId: conversation?.["id"] ?? notificationConversationId,
|
|
739
|
-
conversationName: conversation?.["name"],
|
|
740
|
-
});
|
|
741
|
-
fanout(this.handlers.rawNotification, notification, this.opts.logger);
|
|
742
|
-
// Direct descriptor-keyed dispatch. Each branch narrows on the
|
|
743
|
-
// descriptor identity carried by the decoded notification — same
|
|
744
|
-
// payload-validation guarantee as before, without the extra
|
|
745
|
-
// group-decoder indirection (params were validated upstream by
|
|
746
|
-
// `decodeServerInbound` / `decodeFrames`).
|
|
747
|
-
switch (notification.definition) {
|
|
748
|
-
case MessageReceivedNotificationDefinition:
|
|
749
|
-
this.handleMessageReceivedNotification(notification.params);
|
|
750
|
-
return;
|
|
751
|
-
case ConversationCreatedNotificationDefinition:
|
|
752
|
-
this.handleConversationCreatedNotification(notification.params);
|
|
753
|
-
return;
|
|
754
|
-
case ConversationUpdatedNotificationDefinition:
|
|
755
|
-
this.handleConversationUpdatedNotification(notification.params);
|
|
756
|
-
return;
|
|
757
|
-
case ConversationArchivedNotificationDefinition: {
|
|
758
|
-
const params = notification.params;
|
|
759
|
-
this.markConversationArchived(params.conversationId);
|
|
760
|
-
fanout(this.handlers.conversationArchived, params, this.opts.logger);
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
case ConversationUnarchivedNotificationDefinition: {
|
|
764
|
-
const params = notification.params;
|
|
765
|
-
this.archivedConversationIds.delete(params.conversationId);
|
|
766
|
-
fanout(this.handlers.conversationUnarchived, params, this.opts.logger);
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
case DispatchRelease:
|
|
770
|
-
fanout(this.handlers.dispatchRelease, notification.params, this.opts.logger);
|
|
771
|
-
return;
|
|
772
|
-
case DispatchesConsumed:
|
|
773
|
-
fanout(this.handlers.dispatchesConsumed, notification.params, this.opts.logger);
|
|
774
|
-
return;
|
|
775
|
-
case DispatchesExpired:
|
|
776
|
-
fanout(this.handlers.dispatchesExpired, notification.params, this.opts.logger);
|
|
777
|
-
return;
|
|
778
|
-
default:
|
|
779
|
-
// Other notification descriptors flow through `rawNotification`
|
|
780
|
-
// fanout above; the service has no typed bridge for them.
|
|
781
|
-
return;
|
|
614
|
+
this.recordNotificationTrace(notification);
|
|
615
|
+
fanout(this.handlers.rawNotification, notification);
|
|
616
|
+
this.dispatchTypedNotification(notification);
|
|
617
|
+
}
|
|
618
|
+
recordNotificationTrace(notification) {
|
|
619
|
+
Effect.runFork(appendClientEventTrace(notificationTraceRecord(notification, this._ownAgentId)));
|
|
620
|
+
}
|
|
621
|
+
dispatchTypedNotification(notification) {
|
|
622
|
+
const dispatch = this.notificationDispatchers.get(notification.definition);
|
|
623
|
+
if (dispatch === undefined) {
|
|
624
|
+
return;
|
|
782
625
|
}
|
|
626
|
+
dispatch(notification);
|
|
783
627
|
}
|
|
784
628
|
/**
|
|
785
629
|
* Record `messageId` in the per-conversation dedup window. Returns
|
|
@@ -820,7 +664,7 @@ export class MoltZapService {
|
|
|
820
664
|
// via resolveAgentName(), which populates agentNamesRef on first miss and
|
|
821
665
|
// hits the cache on every subsequent message.
|
|
822
666
|
if (msg.senderId !== this._ownAgentId) {
|
|
823
|
-
fanout(this.handlers.message, msg
|
|
667
|
+
fanout(this.handlers.message, msg);
|
|
824
668
|
}
|
|
825
669
|
}
|
|
826
670
|
handleConversationCreatedNotification(notification) {
|
|
@@ -830,6 +674,14 @@ export class MoltZapService {
|
|
|
830
674
|
handleConversationUpdatedNotification(notification) {
|
|
831
675
|
this.upsertConversationNotification(notification);
|
|
832
676
|
}
|
|
677
|
+
handleConversationArchivedNotification(notification) {
|
|
678
|
+
this.markConversationArchived(notification.conversationId);
|
|
679
|
+
fanout(this.handlers.conversationArchived, notification);
|
|
680
|
+
}
|
|
681
|
+
handleConversationUnarchivedNotification(notification) {
|
|
682
|
+
this.archivedConversationIds.delete(notification.conversationId);
|
|
683
|
+
fanout(this.handlers.conversationUnarchived, notification);
|
|
684
|
+
}
|
|
833
685
|
upsertConversationNotification(notification) {
|
|
834
686
|
const { conversation } = notification;
|
|
835
687
|
this.archivedConversationIds.delete(conversation.id);
|