























@@ -11,12 +11,33 @@ import {
1111resolveMergedAssistantText,
1212shouldSuppressAssistantEventForLiveChat,
1313} from "./live-chat-projector.js";
14+import type {
15+ChatRunState,
16+SessionEventSubscriberRegistry,
17+ToolEventRecipientRegistry,
18+} from "./server-chat-state.js";
1419import { loadGatewaySessionRow } from "./server-chat.load-gateway-session-row.runtime.js";
1520import { persistGatewaySessionLifecycleEvent } from "./server-chat.persist-session-lifecycle.runtime.js";
1621import { deriveGatewaySessionLifecycleSnapshot } from "./session-lifecycle-state.js";
1722import { loadSessionEntry } from "./session-utils.js";
1823import { formatForLog } from "./ws-log.js";
192425+export {
26+createChatRunRegistry,
27+createChatRunState,
28+createSessionEventSubscriberRegistry,
29+createSessionMessageSubscriberRegistry,
30+createToolEventRecipientRegistry,
31+} from "./server-chat-state.js";
32+export type {
33+ChatRunEntry,
34+ChatRunRegistry,
35+ChatRunState,
36+SessionEventSubscriberRegistry,
37+SessionMessageSubscriberRegistry,
38+ToolEventRecipientRegistry,
39+} from "./server-chat-state.js";
40+2041function resolveHeartbeatAckMaxChars(): number {
2142try {
2243const cfg = getRuntimeConfig();
@@ -84,307 +105,12 @@ function normalizeHeartbeatChatFinalText(params: {
84105return { suppress: false, text: stripped.text };
85106}
8610787-export type ChatRunEntry = {
88-sessionKey: string;
89-clientRunId: string;
90-};
91-92-export type ChatRunRegistry = {
93-add: (sessionId: string, entry: ChatRunEntry) => void;
94-peek: (sessionId: string) => ChatRunEntry | undefined;
95-shift: (sessionId: string) => ChatRunEntry | undefined;
96-remove: (sessionId: string, clientRunId: string, sessionKey?: string) => ChatRunEntry | undefined;
97-clear: () => void;
98-};
99-100-export function createChatRunRegistry(): ChatRunRegistry {
101-const chatRunSessions = new Map<string, ChatRunEntry[]>();
102-103-const add = (sessionId: string, entry: ChatRunEntry) => {
104-const queue = chatRunSessions.get(sessionId);
105-if (queue) {
106-queue.push(entry);
107-} else {
108-chatRunSessions.set(sessionId, [entry]);
109-}
110-};
111-112-const peek = (sessionId: string) => chatRunSessions.get(sessionId)?.[0];
113-114-const shift = (sessionId: string) => {
115-const queue = chatRunSessions.get(sessionId);
116-if (!queue || queue.length === 0) {
117-return undefined;
118-}
119-const entry = queue.shift();
120-if (!queue.length) {
121-chatRunSessions.delete(sessionId);
122-}
123-return entry;
124-};
125-126-const remove = (sessionId: string, clientRunId: string, sessionKey?: string) => {
127-const queue = chatRunSessions.get(sessionId);
128-if (!queue || queue.length === 0) {
129-return undefined;
130-}
131-const idx = queue.findIndex(
132-(entry) =>
133-entry.clientRunId === clientRunId && (sessionKey ? entry.sessionKey === sessionKey : true),
134-);
135-if (idx < 0) {
136-return undefined;
137-}
138-const [entry] = queue.splice(idx, 1);
139-if (!queue.length) {
140-chatRunSessions.delete(sessionId);
141-}
142-return entry;
143-};
144-145-const clear = () => {
146-chatRunSessions.clear();
147-};
148-149-return { add, peek, shift, remove, clear };
150-}
151-152-export type ChatRunState = {
153-registry: ChatRunRegistry;
154-rawBuffers: Map<string, string>;
155-buffers: Map<string, string>;
156-deltaSentAt: Map<string, number>;
157-/** Length of text at the time of the last broadcast, used to avoid duplicate flushes. */
158-deltaLastBroadcastLen: Map<string, number>;
159-abortedRuns: Map<string, number>;
160-clear: () => void;
161-};
162-163-export function createChatRunState(): ChatRunState {
164-const registry = createChatRunRegistry();
165-const rawBuffers = new Map<string, string>();
166-const buffers = new Map<string, string>();
167-const deltaSentAt = new Map<string, number>();
168-const deltaLastBroadcastLen = new Map<string, number>();
169-const abortedRuns = new Map<string, number>();
170-171-const clear = () => {
172-registry.clear();
173-rawBuffers.clear();
174-buffers.clear();
175-deltaSentAt.clear();
176-deltaLastBroadcastLen.clear();
177-abortedRuns.clear();
178-};
179-180-return {
181- registry,
182- rawBuffers,
183- buffers,
184- deltaSentAt,
185- deltaLastBroadcastLen,
186- abortedRuns,
187- clear,
188-};
189-}
190-191-export type ToolEventRecipientRegistry = {
192-add: (runId: string, connId: string) => void;
193-get: (runId: string) => ReadonlySet<string> | undefined;
194-markFinal: (runId: string) => void;
195-};
196-197-export type SessionEventSubscriberRegistry = {
198-subscribe: (connId: string) => void;
199-unsubscribe: (connId: string) => void;
200-getAll: () => ReadonlySet<string>;
201-clear: () => void;
202-};
203-204-export type SessionMessageSubscriberRegistry = {
205-subscribe: (connId: string, sessionKey: string) => void;
206-unsubscribe: (connId: string, sessionKey: string) => void;
207-unsubscribeAll: (connId: string) => void;
208-get: (sessionKey: string) => ReadonlySet<string>;
209-clear: () => void;
210-};
211-212-type ToolRecipientEntry = {
213-connIds: Set<string>;
214-updatedAt: number;
215-finalizedAt?: number;
216-};
217-218-const TOOL_EVENT_RECIPIENT_TTL_MS = 10 * 60 * 1000;
219-const TOOL_EVENT_RECIPIENT_FINAL_GRACE_MS = 30 * 1000;
220108/**
221109 * Keep this aligned with the agent.wait lifecycle-error grace so chat surfaces
222110 * do not finalize a run before fallback or retry reuses the same runId.
223111 */
224112const AGENT_LIFECYCLE_ERROR_RETRY_GRACE_MS = 15_000;
225113226-export function createSessionEventSubscriberRegistry(): SessionEventSubscriberRegistry {
227-const connIds = new Set<string>();
228-const empty = new Set<string>();
229-230-return {
231-subscribe: (connId: string) => {
232-const normalized = connId.trim();
233-if (!normalized) {
234-return;
235-}
236-connIds.add(normalized);
237-},
238-unsubscribe: (connId: string) => {
239-const normalized = connId.trim();
240-if (!normalized) {
241-return;
242-}
243-connIds.delete(normalized);
244-},
245-getAll: () => (connIds.size > 0 ? connIds : empty),
246-clear: () => {
247-connIds.clear();
248-},
249-};
250-}
251-252-export function createSessionMessageSubscriberRegistry(): SessionMessageSubscriberRegistry {
253-const sessionToConnIds = new Map<string, Set<string>>();
254-const connToSessionKeys = new Map<string, Set<string>>();
255-const empty = new Set<string>();
256-257-const normalize = (value: string): string => value.trim();
258-259-return {
260-subscribe: (connId: string, sessionKey: string) => {
261-const normalizedConnId = normalize(connId);
262-const normalizedSessionKey = normalize(sessionKey);
263-if (!normalizedConnId || !normalizedSessionKey) {
264-return;
265-}
266-const connIds = sessionToConnIds.get(normalizedSessionKey) ?? new Set<string>();
267-connIds.add(normalizedConnId);
268-sessionToConnIds.set(normalizedSessionKey, connIds);
269-270-const sessionKeys = connToSessionKeys.get(normalizedConnId) ?? new Set<string>();
271-sessionKeys.add(normalizedSessionKey);
272-connToSessionKeys.set(normalizedConnId, sessionKeys);
273-},
274-unsubscribe: (connId: string, sessionKey: string) => {
275-const normalizedConnId = normalize(connId);
276-const normalizedSessionKey = normalize(sessionKey);
277-if (!normalizedConnId || !normalizedSessionKey) {
278-return;
279-}
280-const connIds = sessionToConnIds.get(normalizedSessionKey);
281-if (connIds) {
282-connIds.delete(normalizedConnId);
283-if (connIds.size === 0) {
284-sessionToConnIds.delete(normalizedSessionKey);
285-}
286-}
287-const sessionKeys = connToSessionKeys.get(normalizedConnId);
288-if (sessionKeys) {
289-sessionKeys.delete(normalizedSessionKey);
290-if (sessionKeys.size === 0) {
291-connToSessionKeys.delete(normalizedConnId);
292-}
293-}
294-},
295-unsubscribeAll: (connId: string) => {
296-const normalizedConnId = normalize(connId);
297-if (!normalizedConnId) {
298-return;
299-}
300-const sessionKeys = connToSessionKeys.get(normalizedConnId);
301-if (!sessionKeys) {
302-return;
303-}
304-for (const sessionKey of sessionKeys) {
305-const connIds = sessionToConnIds.get(sessionKey);
306-if (!connIds) {
307-continue;
308-}
309-connIds.delete(normalizedConnId);
310-if (connIds.size === 0) {
311-sessionToConnIds.delete(sessionKey);
312-}
313-}
314-connToSessionKeys.delete(normalizedConnId);
315-},
316-get: (sessionKey: string) => {
317-const normalizedSessionKey = normalize(sessionKey);
318-if (!normalizedSessionKey) {
319-return empty;
320-}
321-return sessionToConnIds.get(normalizedSessionKey) ?? empty;
322-},
323-clear: () => {
324-sessionToConnIds.clear();
325-connToSessionKeys.clear();
326-},
327-};
328-}
329-330-export function createToolEventRecipientRegistry(): ToolEventRecipientRegistry {
331-const recipients = new Map<string, ToolRecipientEntry>();
332-333-const prune = () => {
334-if (recipients.size === 0) {
335-return;
336-}
337-const now = Date.now();
338-for (const [runId, entry] of recipients) {
339-const cutoff = entry.finalizedAt
340- ? entry.finalizedAt + TOOL_EVENT_RECIPIENT_FINAL_GRACE_MS
341- : entry.updatedAt + TOOL_EVENT_RECIPIENT_TTL_MS;
342-if (now >= cutoff) {
343-recipients.delete(runId);
344-}
345-}
346-};
347-348-const add = (runId: string, connId: string) => {
349-if (!runId || !connId) {
350-return;
351-}
352-const now = Date.now();
353-const existing = recipients.get(runId);
354-if (existing) {
355-existing.connIds.add(connId);
356-existing.updatedAt = now;
357-} else {
358-recipients.set(runId, {
359-connIds: new Set([connId]),
360-updatedAt: now,
361-});
362-}
363-prune();
364-};
365-366-const get = (runId: string) => {
367-const entry = recipients.get(runId);
368-if (!entry) {
369-return undefined;
370-}
371-entry.updatedAt = Date.now();
372-prune();
373-return entry.connIds;
374-};
375-376-const markFinal = (runId: string) => {
377-const entry = recipients.get(runId);
378-if (!entry) {
379-return;
380-}
381-entry.finalizedAt = Date.now();
382-prune();
383-};
384-385-return { add, get, markFinal };
386-}
387-388114export type ChatEventBroadcast = (
389115event: string,
390116payload: unknown,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。