Skip to content

Commit 607f34a

Browse files
committed
fix(gui): stabilize remote chat streaming
1 parent d942753 commit 607f34a

7 files changed

Lines changed: 425 additions & 44 deletions

File tree

crates/agent-gui/src-tauri/src/services/gateway.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ impl GatewayController {
559559
worker_id: Option<String>,
560560
) -> Result<(), String> {
561561
if !self.renew_remote_chat_request_lease(&request_id, worker_id.as_deref(), true)? {
562-
return Ok(());
562+
return Err("remote chat request lease is no longer active".to_string());
563563
}
564564
let envelope = build_chat_event_envelope(request_id, event)?;
565565
self.send_agent_envelope(envelope).await

crates/agent-gui/src/lib/system/powerActivity.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ function createActivityId(scope: string) {
1212

1313
async function beginPowerActivity(activityId: string, reason: string) {
1414
await invoke("system_begin_power_activity", {
15-
activityId,
15+
activity_id: activityId,
1616
reason,
17-
ttlMs: POWER_ACTIVITY_TTL_MS,
17+
ttl_ms: POWER_ACTIVITY_TTL_MS,
1818
});
1919
}
2020

@@ -40,7 +40,7 @@ export async function withPowerActivity<T>(scope: string, reason: string, run: (
4040
clearInterval(refreshTimer);
4141
}
4242
try {
43-
await invoke("system_end_power_activity", { activityId });
43+
await invoke("system_end_power_activity", { activity_id: activityId });
4444
} catch (error) {
4545
console.warn("system_end_power_activity failed", error);
4646
}

crates/agent-gui/src/pages/ChatPage.tsx

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,7 +1439,8 @@ export function ChatPage(props: ChatPageProps) {
14391439
scrollAreaRef,
14401440
composerBusyRef,
14411441
});
1442-
const { queueGatewayBridgeEventForRequest } = useGatewayBridgeBatcher();
1442+
const { queueGatewayBridgeEventForRequest, flushGatewayBridgeEventsForRequest } =
1443+
useGatewayBridgeBatcher();
14431444
const {
14441445
currentConversationIdRef,
14451446
conversationRuntimeCacheRef,
@@ -2295,13 +2296,29 @@ export function ChatPage(props: ChatPageProps) {
22952296
);
22962297
inFlightQueuedTurn = null;
22972298
} else if (gatewayRequest) {
2298-
void invoke("gateway_chat_complete", {
2299-
request_id: gatewayRequest.requestId,
2300-
conversation_id: targetConversationId,
2301-
worker_id: gatewayWorkerId,
2302-
} as any).catch((error) => {
2299+
try {
2300+
await flushGatewayBridgeEventsForRequest(gatewayRequest.requestId);
2301+
await invoke("gateway_chat_complete", {
2302+
request_id: gatewayRequest.requestId,
2303+
conversation_id: targetConversationId,
2304+
worker_id: gatewayWorkerId,
2305+
} as any);
2306+
} catch (error) {
23032307
console.warn("gateway_chat_complete failed", error);
2304-
});
2308+
await invoke("gateway_chat_fail", {
2309+
request_id: gatewayRequest.requestId,
2310+
conversation_id: targetConversationId,
2311+
error_code: "desktop_event_flush_failed",
2312+
message: asErrorMessage(
2313+
error,
2314+
"Failed to flush gateway chat events before completing queued request.",
2315+
),
2316+
terminal: true,
2317+
worker_id: gatewayWorkerId,
2318+
} as any).catch((failError) => {
2319+
console.warn("gateway_chat_fail failed", failError);
2320+
});
2321+
}
23052322
}
23062323
return accepted;
23072324
})
@@ -3441,6 +3458,7 @@ export function ChatPage(props: ChatPageProps) {
34413458
ensureGatewayBridgeConversationReadyRef,
34423459
sendActionRef,
34433460
queueGatewayBridgeEventForRequest,
3461+
flushGatewayBridgeEventsForRequest,
34443462
shouldQueueGatewayChatRequest,
34453463
enqueueGatewayChatRequest,
34463464
isConversationRunning,
@@ -3517,12 +3535,54 @@ export function ChatPage(props: ChatPageProps) {
35173535
gatewayBridgeRequest?.requestId ?? createLocalGatewayChatRunId(conversationId);
35183536
const gatewayBridgeWorkerId =
35193537
gatewayBridgeRequest?.workerId ?? (mirrorsLocalRunToGateway ? "gui-live" : undefined);
3538+
let gatewayBridgeAbortSignal: AbortSignal | null = null;
3539+
const waitForLocalGatewayMirror = (sendResult: Promise<void> | void) => {
3540+
const sendPromise = Promise.resolve(sendResult);
3541+
const signal = gatewayBridgeAbortSignal;
3542+
if (!signal) {
3543+
return sendPromise;
3544+
}
3545+
if (signal.aborted) {
3546+
return Promise.resolve();
3547+
}
3548+
return new Promise<void>((resolve, reject) => {
3549+
const handleAbort = () => {
3550+
resolve();
3551+
};
3552+
signal.addEventListener("abort", handleAbort, { once: true });
3553+
sendPromise.then(
3554+
() => {
3555+
signal.removeEventListener("abort", handleAbort);
3556+
resolve();
3557+
},
3558+
(error) => {
3559+
signal.removeEventListener("abort", handleAbort);
3560+
reject(error);
3561+
},
3562+
);
3563+
});
3564+
};
3565+
const sendGatewayBridgeEventForRun = mirrorsLocalRunToGateway
3566+
? (
3567+
requestId: string,
3568+
event: Record<string, unknown>,
3569+
options?: { workerId?: string },
3570+
) => {
3571+
const sendPromise = waitForLocalGatewayMirror(
3572+
queueGatewayBridgeEventForRequest(requestId, event, options),
3573+
);
3574+
sendPromise.catch((error) => {
3575+
console.warn("local gateway chat event mirror failed", error);
3576+
});
3577+
return sendPromise;
3578+
}
3579+
: queueGatewayBridgeEventForRequest;
35203580
const gatewayBridgeEvents = createGatewayBridgeEventController({
35213581
conversationId,
35223582
requestId: gatewayBridgeRequestId,
35233583
workerId: gatewayBridgeWorkerId,
35243584
enabled: Boolean(gatewayBridgeRequest) || hasRemoteGatewayTarget,
3525-
sendEvent: queueGatewayBridgeEventForRequest,
3585+
sendEvent: sendGatewayBridgeEventForRun,
35263586
resolveErrorConversationId: () =>
35273587
gatewayBridgeRequest?.conversationId ?? currentConversationIdRef.current,
35283588
});
@@ -3696,6 +3756,7 @@ export function ChatPage(props: ChatPageProps) {
36963756
const conversationThrottleState = getCompactionThrottleState(conversationId);
36973757
const isConversationVisible = () => currentConversationIdRef.current === conversationId;
36983758
let requestController = new AbortController();
3759+
gatewayBridgeAbortSignal = requestController.signal;
36993760
const conversationDebugLogger = createStreamDebugLogger({
37003761
enabled: effectiveIsAgentDevExecutionMode,
37013762
conversationId,
@@ -3821,6 +3882,22 @@ export function ChatPage(props: ChatPageProps) {
38213882
queueGatewayConversationActivity(conversationId, false, conversationCwd);
38223883
}
38233884
}
3885+
function finishAbortedBeforeRuntimeStart() {
3886+
if (!requestController.signal.aborted) {
3887+
return false;
3888+
}
3889+
gatewayBridgeEvents.queueEvent({
3890+
type: "error",
3891+
message: "Cancelled",
3892+
conversation_id: conversationId,
3893+
});
3894+
gatewayBridgeEvents.close();
3895+
updateToolStatus(null, transcriptStore, isConversationVisible());
3896+
clearAbortSnapshot(transcriptStore);
3897+
markConversationRunStopped();
3898+
requestQueuedChatTurnProcessing(conversationId);
3899+
return true;
3900+
}
38243901
let localGatewayRunStarted = false;
38253902
async function markLocalGatewayRunStarted() {
38263903
if (!mirrorsLocalRunToGateway || localGatewayRunStarted) {
@@ -3836,9 +3913,18 @@ export function ChatPage(props: ChatPageProps) {
38363913
markConversationRunStarted();
38373914
if (mirrorsLocalRunToGateway) {
38383915
try {
3839-
await markLocalGatewayRunStarted();
3916+
await waitForLocalGatewayMirror(markLocalGatewayRunStarted());
38403917
} catch (error) {
3918+
const message = asErrorMessage(error, "Gateway 本地会话启动同步失败");
3919+
setConversationErrorState(message);
3920+
gatewayBridgeEvents.emitError(message, conversationId);
3921+
gatewayBridgeEvents.close();
3922+
markConversationRunStopped();
38413923
console.warn("gateway_chat_mark_local_started failed", error);
3924+
return true;
3925+
}
3926+
if (finishAbortedBeforeRuntimeStart()) {
3927+
return true;
38423928
}
38433929
}
38443930
if (overrides?.beforeRuntimeStart) {
@@ -3852,6 +3938,9 @@ export function ChatPage(props: ChatPageProps) {
38523938
markConversationRunStopped();
38533939
return false;
38543940
}
3941+
if (finishAbortedBeforeRuntimeStart()) {
3942+
return true;
3943+
}
38553944
}
38563945

38573946
// Persist the user turn immediately so WebUI/GUI sidebars can surface the
@@ -3920,7 +4009,22 @@ export function ChatPage(props: ChatPageProps) {
39204009
console.warn("gateway stream started before initial user turn was persisted");
39214010
}
39224011
}
3923-
await gatewayBridgeEvents.queueUserMessage(text, uploadedFiles);
4012+
try {
4013+
await gatewayBridgeEvents.queueUserMessage(text, uploadedFiles);
4014+
} catch (error) {
4015+
if (!mirrorsLocalRunToGateway) {
4016+
throw error;
4017+
}
4018+
const message = asErrorMessage(error, "Gateway 用户消息同步失败");
4019+
setConversationErrorState(message);
4020+
gatewayBridgeEvents.emitError(message, conversationId);
4021+
gatewayBridgeEvents.close();
4022+
markConversationRunStopped();
4023+
return true;
4024+
}
4025+
if (finishAbortedBeforeRuntimeStart()) {
4026+
return true;
4027+
}
39244028
acknowledgeGatewayRunStarted();
39254029
let activeCompactionRollback: {
39264030
state: ConversationViewState;
@@ -4217,6 +4321,7 @@ export function ChatPage(props: ChatPageProps) {
42174321

42184322
function renewRequestController() {
42194323
requestController = new AbortController();
4324+
gatewayBridgeAbortSignal = requestController.signal;
42204325
setConversationAbortController(conversationId, requestController);
42214326
return requestController;
42224327
}
@@ -4489,6 +4594,9 @@ export function ChatPage(props: ChatPageProps) {
44894594
hookWarning: null,
44904595
}));
44914596
}
4597+
if (finishAbortedBeforeRuntimeStart()) {
4598+
return true;
4599+
}
44924600

44934601
try {
44944602
if (effectiveIsAgentMode) {

0 commit comments

Comments
 (0)