























@@ -301,6 +301,30 @@ export function createEventHandlers(context: EventHandlerContext) {
301301}
302302};
303303304+const promoteMostRecentSessionRun = (): boolean => {
305+if (state.activeChatRunId || sessionRuns.size === 0) {
306+return false;
307+}
308+let nextRunId: string | undefined;
309+let nextSeenAt = -1;
310+for (const [runId, seenAt] of sessionRuns) {
311+if (seenAt > nextSeenAt) {
312+nextRunId = runId;
313+nextSeenAt = seenAt;
314+}
315+}
316+if (!nextRunId) {
317+return false;
318+}
319+// A concurrent run can outlive the active run. Keep the activity owner on
320+// remaining work so terminal cleanup cannot incorrectly return the TUI idle.
321+state.activeChatRunId = nextRunId;
322+clearStreamingWatchdog();
323+setActivityStatus("running");
324+armStreamingWatchdog(nextRunId);
325+return true;
326+};
327+304328const clearStaleStreamingIfNoTrackedRunRemains = () => {
305329const activeRunId = state.activeChatRunId;
306330// A missing active run is the recovery case; only tracked active runs block cleanup.
@@ -344,15 +368,18 @@ export function createEventHandlers(context: EventHandlerContext) {
344368}) => {
345369noteFinalizedRun(params.runId, { displayedFinal: params.displayedFinal });
346370clearActiveRunIfMatch(params.runId);
371+const promotedRemainingRun = promoteMostRecentSessionRun();
347372flushPendingHistoryRefreshIfIdle();
348-if (params.wasActiveRun) {
349-setActivityStatus(params.status);
350-clearStreamingWatchdog();
351-} else {
352-if (streamingWatchdogRunId === params.runId) {
373+if (!promotedRemainingRun) {
374+if (params.wasActiveRun) {
375+setActivityStatus(params.status);
353376clearStreamingWatchdog();
377+} else {
378+if (streamingWatchdogRunId === params.runId) {
379+clearStreamingWatchdog();
380+}
381+clearStaleStreamingIfNoTrackedRunRemains();
354382}
355-clearStaleStreamingIfNoTrackedRunRemains();
356383}
357384void refreshSessionInfo?.();
358385};
@@ -367,12 +394,15 @@ export function createEventHandlers(context: EventHandlerContext) {
367394streamAssembler.drop(params.runId);
368395sessionRuns.delete(params.runId);
369396clearActiveRunIfMatch(params.runId);
397+const promotedRemainingRun = promoteMostRecentSessionRun();
370398flushPendingHistoryRefreshIfIdle();
371-if (params.wasActiveRun) {
372-setActivityStatus(params.status);
373-clearStreamingWatchdog();
374-} else if (streamingWatchdogRunId === params.runId) {
375-clearStreamingWatchdog();
399+if (!promotedRemainingRun) {
400+if (params.wasActiveRun) {
401+setActivityStatus(params.status);
402+clearStreamingWatchdog();
403+} else if (streamingWatchdogRunId === params.runId) {
404+clearStreamingWatchdog();
405+}
376406}
377407void refreshSessionInfo?.();
378408};
@@ -419,10 +449,7 @@ export function createEventHandlers(context: EventHandlerContext) {
419449420450const hasConcurrentActiveRun = (runId: string) => {
421451const activeRunId = state.activeChatRunId;
422-if (!activeRunId || activeRunId === runId) {
423-return false;
424-}
425-return sessionRuns.has(activeRunId);
452+return Boolean(activeRunId && activeRunId !== runId);
426453};
427454428455const maybeRefreshHistoryForRun = (
@@ -699,6 +726,33 @@ export function createEventHandlers(context: EventHandlerContext) {
699726}
700727const evt = payload as AgentEvent;
701728syncSessionKey();
729+// System-injected runs (bridge-notify, webhook, cron) never go through the
730+// TUI submit path, so no active/pending run id exists when their lifecycle
731+// "start" arrives — leaving the status bar idle until the response lands.
732+// Adopt such a run for the current session (lifecycle events always carry
733+// sessionKey) so the activity indicator shows work is happening, mirroring
734+// how chat deltas adopt runs in handleChatEvent. Only claim the active slot
735+// when none is held, so a concurrent user run keeps the indicator.
736+const isUntrackedRun =
737+evt.runId !== state.activeChatRunId &&
738+evt.runId !== state.pendingChatRunId &&
739+!sessionRuns.has(evt.runId) &&
740+!finalizedRuns.has(evt.runId);
741+if (
742+isUntrackedRun &&
743+evt.stream === "lifecycle" &&
744+asString(evt.data?.phase, "") === "start" &&
745+!(isLocalBtwRunId?.(evt.runId) ?? false) &&
746+isSameSessionKey(evt.sessionKey, state.currentSessionKey) &&
747+isMatchingGlobalAgentEvent(evt.sessionKey, evt.agentId)
748+) {
749+noteSessionRun(evt.runId);
750+// Mirror handleChatEvent: side-question (btw) runs never claim the active
751+// slot, so a concurrent btw run cannot hijack the main activity indicator.
752+if (!state.activeChatRunId) {
753+state.activeChatRunId = evt.runId;
754+}
755+}
702756// Agent events (tool streaming, lifecycle) are emitted per-run. Filter against the
703757// active chat run id, not the session id. Tool results can arrive after the chat
704758// final event, so accept finalized runs for tool updates.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。