export class MoltZapService {
private client: MoltZapAgentClient | null = null;
private _connected = false;
/**
* Service-owned scope (spec #596 / Spec B §"4.2 service.ts" lifecycle
* reshape). Opened in `connect()`, owns the `subscribeAll → Stream.runForEach`
* fan-out fiber. Closed in `close()` so the fiber terminates with the
* service.
*
* Held off the public `connect()` signature so callers do not need to
* thread a `Scope` requirement.
*/
private serviceScope: Scope.CloseableScope | null = null;
private readonly conversationsRef: Ref.Ref<
HashMap.HashMap<string, ConversationMeta>
> = Effect.runSync(Ref.make(HashMap.empty<string, ConversationMeta>()));
private readonly messagesRef: Ref.Ref<
HashMap.HashMap<string, ReadonlyArray<Message>>
> = Effect.runSync(Ref.make(HashMap.empty<string, ReadonlyArray<Message>>()));
private readonly agentNamesRef: Ref.Ref<HashMap.HashMap<string, string>> =
Effect.runSync(Ref.make(HashMap.empty<string, string>()));
private readonly agentConversationCacheRef: Ref.Ref<
HashMap.HashMap<
string,
{ readonly taskId: TaskId; readonly conversationId: ConversationId }
>
> = Effect.runSync(
Ref.make(
HashMap.empty<
string,
{ readonly taskId: TaskId; readonly conversationId: ConversationId }
>(),
),
);
private readonly lastNotifiedRef: Ref.Ref<
HashMap.HashMap<string, HashMap.HashMap<string, string>>
> = Effect.runSync(
Ref.make(HashMap.empty<string, HashMap.HashMap<string, string>>()),
);
private readonly lastReadRef: Ref.Ref<
HashMap.HashMap<string, HashMap.HashMap<string, ReadonlySet<string>>>
> = Effect.runSync(
Ref.make(
HashMap.empty<string, HashMap.HashMap<string, ReadonlySet<string>>>(),
),
);
private readonly archivedConversationIds = new Set<string>();
/**
* Insertion-ordered set of recently seen messageIds per conversation.
* Bounded at DEDUP_WINDOW_PER_CONV entries per conversation; oldest entry
* is evicted when the window is full. Set#keys() preserves insertion
* order in V8 / the spec, so eviction via `.next()` is O(1).
*
* Keyed and valued by their branded ids so the compiler rejects a
* `MessageId` accidentally used as a conversation key (or vice versa).
*/
private readonly seenMessageIds = new Map<ConversationId, Set<MessageId>>();
private readonly handlers: {
[K in ServiceHandlerName]: Array<
NotificationHandler<ServiceHandlerPayloads[K]>
>;
} = {
message: [],
rawNotification: [],
disconnect: [],
reconnect: [],
conversationArchived: [],
conversationUnarchived: [],
dispatchRelease: [],
dispatchesConsumed: [],
dispatchesExpired: [],
};
private readonly notificationDispatchers = new Map<
AnyNotificationDefinition,
NotificationDispatcher
>([
[
MessageReceivedNotificationDefinition,
(notification) =>
this.handleMessageReceivedNotification(
notification.params as MessageReceivedNotification,
),
],
[
TaskConversationCreatedNotificationDefinition,
(notification) =>
this.handleConversationCreatedNotification(
notification.params as TaskConversationCreatedNotification,
),
],
[
TaskConversationArchivedNotificationDefinition,
(notification) =>
this.handleConversationArchivedNotification(
notification.params as TaskConversationArchivedNotification,
),
],
[
TaskConversationUnarchivedNotificationDefinition,
(notification) =>
this.handleConversationUnarchivedNotification(
notification.params as TaskConversationUnarchivedNotification,
),
],
[
DispatchRelease,
(notification) =>
fanout(
this.handlers.dispatchRelease,
notification.params as NotificationParamsOf<typeof DispatchRelease>,
),
],
[
DispatchesConsumed,
(notification) =>
fanout(
this.handlers.dispatchesConsumed,