perf: 优化 Agent 流式会话处理

- 合并流式通知与持久化写入,限制浏览器会话缓存

- 增量投影轮次事件并稳定关联异步工具任务
This commit is contained in:
2026-07-27 19:41:11 +08:00
parent 5497931abd
commit b08eb009bb
10 changed files with 539 additions and 47 deletions

View File

@@ -64,10 +64,15 @@ interface StartOptions {
}
const STORAGE_VERSION = 2;
const STREAM_NOTIFY_INTERVAL_MS = 50;
const STREAM_PERSIST_INTERVAL_MS = 300;
const MAX_RUNTIME_SESSIONS_PER_IDENTITY = 10;
const sessions = new Map<string, RuntimeSessionState>();
const listeners = new Set<() => void>();
const latestSessionIds = new Map<string, string>();
const persistTimers = new Map<string, ReturnType<typeof setTimeout>>();
let notifyTimer: ReturnType<typeof setTimeout> | undefined;
function clone<T>(value: T): T {
const serialized = JSON.stringify(value);
@@ -108,6 +113,24 @@ function notify() {
}
}
function notifyNow() {
if (notifyTimer) {
clearTimeout(notifyTimer);
notifyTimer = undefined;
}
notify();
}
function scheduleNotify() {
if (notifyTimer || listeners.size === 0) {
return;
}
notifyTimer = setTimeout(() => {
notifyTimer = undefined;
notify();
}, STREAM_NOTIFY_INTERVAL_MS);
}
function persistSession(state: RuntimeSessionState) {
const storage = safeSessionStorage();
if (!storage) {
@@ -118,7 +141,7 @@ function persistSession(state: RuntimeSessionState) {
agentName: state.agentName,
completed: state.completed,
error: state.error,
items: clone(state.items),
items: state.items,
prompt: state.prompt,
roundId: state.roundId,
sessionId: state.sessionId,
@@ -136,6 +159,82 @@ function persistSession(state: RuntimeSessionState) {
}
}
function cancelPersistTimer(scopedSessionKey: string) {
const timer = persistTimers.get(scopedSessionKey);
if (!timer) {
return;
}
clearTimeout(timer);
persistTimers.delete(scopedSessionKey);
}
function removeSessionCache(
scopedSessionKey: string,
state: RuntimeSessionState,
) {
cancelPersistTimer(scopedSessionKey);
sessions.delete(scopedSessionKey);
try {
safeSessionStorage()?.removeItem(
storageKey(state.identity, state.sessionId),
);
} catch {
// 本地缓存清理失败不影响服务端会话。
}
}
function pruneRuntimeSessions(identity: string, currentSessionId: string) {
const scopedSessions = [...sessions.entries()]
.filter(([, session]) => session.identity === identity)
.sort((first, second) => second[1].updatedAt - first[1].updatedAt);
if (scopedSessions.length <= MAX_RUNTIME_SESSIONS_PER_IDENTITY) {
return;
}
const protectedKeys = new Set(
scopedSessions
.filter(
([, session]) =>
session.sending || session.sessionId === currentSessionId,
)
.map(([key]) => key),
);
let retainedCount = protectedKeys.size;
for (const [key, session] of scopedSessions) {
if (protectedKeys.has(key)) {
continue;
}
if (retainedCount < MAX_RUNTIME_SESSIONS_PER_IDENTITY) {
retainedCount += 1;
continue;
}
removeSessionCache(key, session);
}
}
function touchState(state: RuntimeSessionState) {
state.updatedAt = Date.now();
latestSessionIds.set(state.identity, state.sessionId);
sessions.set(sessionKey(state.identity, state.sessionId), state);
}
function scheduleStateUpdate(state: RuntimeSessionState) {
touchState(state);
const scopedSessionKey = sessionKey(state.identity, state.sessionId);
if (!persistTimers.has(scopedSessionKey)) {
persistTimers.set(
scopedSessionKey,
setTimeout(() => {
persistTimers.delete(scopedSessionKey);
const current = sessions.get(scopedSessionKey);
if (current) {
persistSession(current);
}
}, STREAM_PERSIST_INTERVAL_MS),
);
}
scheduleNotify();
}
function restoreSession(identity: string, sessionId: string) {
if (!identity) {
return undefined;
@@ -179,11 +278,11 @@ function restoreSession(identity: string, sessionId: string) {
}
function upsertState(state: RuntimeSessionState) {
state.updatedAt = Date.now();
latestSessionIds.set(state.identity, state.sessionId);
sessions.set(sessionKey(state.identity, state.sessionId), state);
touchState(state);
cancelPersistTimer(sessionKey(state.identity, state.sessionId));
persistSession(state);
notify();
pruneRuntimeSessions(state.identity, state.sessionId);
notifyNow();
}
function runningSession(identity = identityScope()) {
@@ -272,11 +371,12 @@ function replaceAcceptedImages(
onAgentChatCacheClear((identity) => {
for (const [key, session] of sessions) {
if (session.identity === identity) {
cancelPersistTimer(key);
sessions.delete(key);
}
}
latestSessionIds.delete(identity);
notify();
notifyNow();
});
export const agentChatRuntimeManager = {
@@ -383,7 +483,7 @@ export const agentChatRuntimeManager = {
void options.onInputAccepted?.();
}
applyAgentSseEnvelope(current.items, envelope, { roundId });
upsertState(current);
scheduleStateUpdate(current);
},
},
);