mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +00:00
fix(opencode): surface stall errors and clear thinking spinner (#869)
* test: reproduce issue #865 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(opencode): surface stall errors and clear thinking spinner (closes #865) Route quota/rate-limit/HTTP-2 cancel stderr through error-styled agent messages, cancel the in-flight prompt, and clear thinking so the web UI does not stay stuck while OpenCode retries upstream. Co-authored-by: Cursor <cursoragent@cursor.com> * test(opencode): non-stall stderr surfaces error without canceling prompt Co-authored-by: Cursor <cursoragent@cursor.com> * fix(acp): use agent-neutral retry stderr message in shared transport AcpStdioTransport is shared by Cursor, Gemini, Kimi, and OpenCode. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(opencode): surface stall errors and clear thinking spinner * fix(acp): parse split stderr stall records Buffer stderr through newline-delimited records so split retry and HTTP/2 cancel signatures still clear stalled OpenCode turns, and retain one web error presentation branch. Verified: targeted ACP and presentation tests plus CLI/web typechecks. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(acp): emit stalled stderr tails immediately Classify buffered retry and HTTP/2 cancellation tails as soon as their signatures are complete, without waiting for the ACP process to close. Verified: targeted ACP transport test and CLI typecheck. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(acp): flush newline-free quota errors * fix(acp): surface newline-free stderr errors * fix(acp): scope stall cancellation and bound stderr * fix(opencode): bind stall cancellation to prompt RPC * fix(acp): preserve partial stderr until classification * fix(acp): report complete cancellation records --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,8 @@ import { randomUUID } from 'node:crypto';
|
||||
import { registerAcpSessionTitleSync } from '@/agent/acpSessionTitle';
|
||||
import { logger } from '@/ui/logger';
|
||||
import { buildHapiMcpBridge } from '@/codex/utils/buildHapiMcpBridge';
|
||||
import type { AcpStderrError } from '@/agent/backends/acp/AcpStdioTransport';
|
||||
import { isAcpStallStderrError } from '@/agent/backends/acp/acpStderrErrors';
|
||||
import { convertAgentMessage } from '@/agent/messageConverter';
|
||||
import type { AgentMessage, McpServerStdio, PromptContent } from '@/agent/types';
|
||||
import { RemoteLauncherBase, type RemoteLauncherDisplayContext, type RemoteLauncherExitReason } from '@/modules/common/remote/RemoteLauncherBase';
|
||||
@@ -121,6 +123,8 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
private defaultBackendEffort: string | null = null;
|
||||
private setModelSupported: boolean | undefined = undefined;
|
||||
private setEffortSupported: boolean | undefined = undefined;
|
||||
private activeAcpSessionId: string | null = null;
|
||||
private stallErrorReportedForPrompt = false;
|
||||
|
||||
constructor(
|
||||
session: OpencodeSession,
|
||||
@@ -170,9 +174,7 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
registerAcpSessionTitleSync(backend, session.client);
|
||||
|
||||
backend.onStderrError((error) => {
|
||||
logger.debug('[opencode-remote] stderr error', error);
|
||||
session.sendSessionEvent({ type: 'message', message: error.message });
|
||||
messageBuffer.addMessage(error.message, 'status');
|
||||
this.handleAcpStderrError(error);
|
||||
});
|
||||
|
||||
await backend.initialize();
|
||||
@@ -205,6 +207,7 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
});
|
||||
}
|
||||
session.onSessionFound(acpSessionId);
|
||||
this.activeAcpSessionId = acpSessionId;
|
||||
|
||||
// Seed currentBackendModel from the ACP session metadata so the first
|
||||
// batch — whose model the hub mirrors from the just-discovered session —
|
||||
@@ -571,6 +574,7 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
text: messageText
|
||||
}];
|
||||
|
||||
this.stallErrorReportedForPrompt = false;
|
||||
session.onThinkingChange(true);
|
||||
|
||||
try {
|
||||
@@ -580,11 +584,7 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
void backend.refreshSessionInfo(acpSessionId, session.path);
|
||||
} catch (error) {
|
||||
logger.warn('[opencode-remote] prompt failed', error);
|
||||
session.sendSessionEvent({
|
||||
type: 'message',
|
||||
message: 'OpenCode prompt failed. Check logs for details.'
|
||||
});
|
||||
messageBuffer.addMessage('OpenCode prompt failed', 'status');
|
||||
this.surfaceAgentError('OpenCode prompt failed. Check logs for details.');
|
||||
} finally {
|
||||
session.onThinkingChange(false);
|
||||
await this.permissionHandler?.cancelAll('Prompt finished');
|
||||
@@ -656,6 +656,42 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
// replacement.
|
||||
}
|
||||
|
||||
private handleAcpStderrError(error: AcpStderrError): void {
|
||||
logger.debug('[opencode-remote] stderr error', error);
|
||||
const isStall = isAcpStallStderrError(error)
|
||||
&& this.backend?.isPromptRequestInFlight() === true;
|
||||
if (isStall && this.stallErrorReportedForPrompt) {
|
||||
return;
|
||||
}
|
||||
if (isStall) {
|
||||
this.stallErrorReportedForPrompt = true;
|
||||
}
|
||||
this.surfaceAgentError(error.message);
|
||||
if (isStall) {
|
||||
void this.clearStalledPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
private surfaceAgentError(message: string): void {
|
||||
this.session.sendAgentMessage({ type: 'error', message });
|
||||
this.messageBuffer.addMessage(message, 'status');
|
||||
}
|
||||
|
||||
private async clearStalledPrompt(): Promise<void> {
|
||||
const backend = this.backend;
|
||||
const sessionId = this.activeAcpSessionId;
|
||||
if (!backend || !sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.session.onThinkingChange(false);
|
||||
try {
|
||||
await backend.cancelPrompt(sessionId);
|
||||
} catch (error) {
|
||||
logger.debug('[opencode-remote] cancelPrompt after stderr failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
private rollbackReasoningEffort(batch: { mode: OpencodeMode }, effort: string | null): void {
|
||||
batch.mode.modelReasoningEffort = effort;
|
||||
this.session.setModelReasoningEffort(effort);
|
||||
|
||||
Reference in New Issue
Block a user