fix(gateway): surface chat.send lifecycle errors to clients

chat.send created the abort controller but never registered the run
in chatRunState.registry. Lifecycle phase=error events then hit the
skipChatErrorFinal = isChatSendRunActive(runId) && !chatLink guard in
server-chat.ts and were dropped, so TUI/Web UI hung in 'waiting' on any
agent failure (401 auth, billing, timeout). /abort reported
'no active run' because no chat event ever reached the client.

Register the chat run right after the abort controller is created so
finalizeLifecycleEvent can resolve chatLink and emit chat.state='error'
as expected. Cleanup is already handled by finalizeLifecycleEvent's
registry.shift and abortChatRun's removeChatRun.

Also patch createChatContext() in chat.directive-tags.test.ts: the
harness was missing addChatRun even though SharedChatContext requires
it, which broke 37 chat.send tests after the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
wangshu94
2026-04-21 22:49:22 +08:00
committed by Mason Huang
parent 5ca6b3568c
commit df2325c19b
2 changed files with 9 additions and 0 deletions

View File

@@ -307,6 +307,7 @@ function createChatContext(): Pick<
| "chatDeltaSentAt"
| "chatDeltaLastBroadcastLen"
| "chatAbortedRuns"
| "addChatRun"
| "removeChatRun"
| "dedupe"
| "loadGatewayModelCatalog"
@@ -322,6 +323,7 @@ function createChatContext(): Pick<
chatDeltaSentAt: new Map(),
chatDeltaLastBroadcastLen: new Map(),
chatAbortedRuns: new Map(),
addChatRun: vi.fn(),
removeChatRun: vi.fn(),
dedupe: new Map(),
loadGatewayModelCatalog: async () =>

View File

@@ -2248,6 +2248,10 @@ export const chatHandlers: GatewayRequestHandlers = {
ownerConnId: normalizeOptionalText(client?.connId),
ownerDeviceId: normalizeOptionalText(client?.connect?.device?.id),
});
context.addChatRun(clientRunId, {
sessionKey,
clientRunId,
});
const ackPayload = {
runId: clientRunId,
status: "started" as const,
@@ -2740,8 +2744,11 @@ export const chatHandlers: GatewayRequestHandlers = {
})
.finally(() => {
context.chatAbortControllers.delete(clientRunId);
context.removeChatRun(clientRunId, clientRunId, sessionKey);
});
} catch (err) {
context.chatAbortControllers.delete(clientRunId);
context.removeChatRun(clientRunId, clientRunId, sessionKey);
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
const payload = {
runId: clientRunId,