






















@@ -228,41 +228,42 @@ export async function drainActiveSessionsForShutdown(params: {
228228);
229229const emittedSessionIds: string[] = [];
230230const hookRunner = getGlobalHookRunner();
231+let settledEmissions = 0;
231232// Inline the session_end emission instead of calling
232233// `emitGatewaySessionEndPluginHook`, because that helper uses fire-and-forget
233-// (`void hookRunner.runSessionEnd(...)`). The shutdown drain must actually
234-// await each plugin handler so the bounded total-timeout races real plugin
235-// work, not just a synchronous for-loop -- otherwise the close handler can
236-// proceed to subsystem teardown while a database-writing `session_end`
237-// plugin is still in flight, which is the original ghost-session failure.
238-const drain = (async () => {
239-for (const entry of tracked) {
240-forgetActiveSessionForShutdown(entry.sessionId);
241-emittedSessionIds.push(entry.sessionId);
242-if (!hookRunner?.hasHooks("session_end")) {
243-continue;
244-}
245-const transcript = resolveStableSessionEndTranscript({
246-sessionId: entry.sessionId,
247-storePath: entry.storePath,
248-sessionFile: entry.sessionFile,
249-agentId: entry.agentId,
250-});
251-const payload = buildSessionEndHookPayload({
252-sessionId: entry.sessionId,
253-sessionKey: entry.sessionKey,
254-cfg: entry.cfg,
255-reason: params.reason,
256-sessionFile: transcript.sessionFile,
257-transcriptArchived: transcript.transcriptArchived,
258-});
234+// (`void hookRunner.runSessionEnd(...)`). Start every tracked session's
235+// emission before awaiting the bounded aggregate so one slow plugin write
236+// cannot prevent later active sessions from receiving `session_end`.
237+const drain = Promise.allSettled(
238+tracked.map(async (entry) => {
259239try {
240+forgetActiveSessionForShutdown(entry.sessionId);
241+emittedSessionIds.push(entry.sessionId);
242+if (!hookRunner?.hasHooks("session_end")) {
243+return;
244+}
245+const transcript = resolveStableSessionEndTranscript({
246+sessionId: entry.sessionId,
247+storePath: entry.storePath,
248+sessionFile: entry.sessionFile,
249+agentId: entry.agentId,
250+});
251+const payload = buildSessionEndHookPayload({
252+sessionId: entry.sessionId,
253+sessionKey: entry.sessionKey,
254+cfg: entry.cfg,
255+reason: params.reason,
256+sessionFile: transcript.sessionFile,
257+transcriptArchived: transcript.transcriptArchived,
258+});
260259await hookRunner.runSessionEnd(payload.event, payload.context);
261260} catch (err) {
262261logVerbose(`session_end hook failed during shutdown drain: ${String(err)}`);
262+} finally {
263+settledEmissions++;
263264}
264-}
265-})();
265+}),
266+);
266267let timer: ReturnType<typeof setTimeout> | undefined;
267268const timeout = new Promise<"timeout">((resolve) => {
268269timer = setTimeout(() => resolve("timeout"), totalTimeoutMs);
@@ -272,7 +273,7 @@ export async function drainActiveSessionsForShutdown(params: {
272273const result = await Promise.race([drain.then(() => "ok" as const), timeout]);
273274if (result === "timeout") {
274275logVerbose(
275-`shutdown session-end drain timed out after ${totalTimeoutMs}ms with ${tracked.length - emittedSessionIds.length} sessions remaining`,
276+`shutdown session-end drain timed out after ${totalTimeoutMs}ms with ${tracked.length - settledEmissions} session_end handler(s) still pending`,
276277);
277278return { emittedSessionIds, timedOut: true };
278279}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。