fix: stream bash output and harden OpenCode connectivity (#2522)

* fix(ui): stream bash tool output while running

* perf(ui): render streaming bash output incrementally

* fix(ui): keep tool duration timer running

* fix(web): recover stalled OpenCode SSE streams

* fix(web): prevent OpenCode restart storms

* fix: address streaming recovery review
This commit is contained in:
Bohdan Triapitsyn
2026-07-29 17:26:11 +03:00
committed by GitHub
parent c84305bf7e
commit 0f830f8804
12 changed files with 372 additions and 41 deletions
+32 -2
View File
@@ -6,6 +6,9 @@ import {
shouldForwardProxyResponseHeader,
} from '../../proxy-headers.js';
import { createRealpathCache } from '../path-realpath-cache.js';
import { DEFAULT_UPSTREAM_STALL_TIMEOUT_MS } from '../event-stream/upstream-reader.js';
const DEFAULT_SSE_HEARTBEAT_INTERVAL_MS = 20_000;
export const createDirectoryQueryCanonicalizer = ({ realpath, ...cacheOptions } = {}) => {
const realpathCache = createRealpathCache({ fallbackOnError: true, realpath, ...cacheOptions });
@@ -186,6 +189,9 @@ export const registerOpenCodeProxy = (app, deps) => {
getOpenCodeAuthHeaders,
buildOpenCodeUrl,
ensureOpenCodeApiPrefix,
SSE_HEARTBEAT_INTERVAL_MS = DEFAULT_SSE_HEARTBEAT_INTERVAL_MS,
SSE_UPSTREAM_STALL_TIMEOUT_MS = DEFAULT_UPSTREAM_STALL_TIMEOUT_MS,
getSseUpstreamStallTimeoutMs = () => SSE_UPSTREAM_STALL_TIMEOUT_MS,
} = deps;
if (app.get('opencodeProxyConfigured')) {
@@ -340,6 +346,8 @@ export const registerOpenCodeProxy = (app, deps) => {
let upstream = null;
let reader = null;
let heartbeatTimer = null;
let upstreamStallTimer = null;
let didUpstreamStall = false;
let writeQueue = Promise.resolve(true);
const sseBoundary = createSseBoundaryTracker();
@@ -392,8 +400,6 @@ export const registerOpenCodeProxy = (app, deps) => {
res.socket.setNoDelay(true);
}
const SSE_HEARTBEAT_INTERVAL_MS = 20_000;
const scheduleHeartbeat = () => {
heartbeatTimer = setTimeout(async () => {
if (abortController.signal.aborted || res.writableEnded || res.destroyed) {
@@ -410,6 +416,20 @@ export const registerOpenCodeProxy = (app, deps) => {
}, SSE_HEARTBEAT_INTERVAL_MS);
};
const clearUpstreamStallTimer = () => {
clearTimeout(upstreamStallTimer);
upstreamStallTimer = null;
};
const resetUpstreamStallTimer = () => {
clearUpstreamStallTimer();
upstreamStallTimer = setTimeout(() => {
didUpstreamStall = true;
abortController.abort();
}, getSseUpstreamStallTimeoutMs());
upstreamStallTimer.unref?.();
};
const enqueueSseWrite = (value) => {
writeQueue = writeQueue
.catch(() => false)
@@ -423,6 +443,7 @@ export const registerOpenCodeProxy = (app, deps) => {
};
scheduleHeartbeat();
resetUpstreamStallTimer();
reader = upstream.body.getReader();
while (!abortController.signal.aborted) {
@@ -431,6 +452,7 @@ export const registerOpenCodeProxy = (app, deps) => {
break;
}
if (value && value.length > 0) {
resetUpstreamStallTimer();
sseBoundary.observe(value);
const canContinue = await enqueueSseWrite(value);
if (!canContinue) {
@@ -442,6 +464,10 @@ export const registerOpenCodeProxy = (app, deps) => {
res.end();
} catch (error) {
if (isAbortError(error)) {
if (didUpstreamStall && !res.writableEnded && !res.destroyed) {
await writeQueue.catch(() => false);
res.end();
}
return;
}
console.error('[proxy] OpenCode SSE proxy error:', error?.message ?? error);
@@ -455,6 +481,10 @@ export const registerOpenCodeProxy = (app, deps) => {
clearTimeout(heartbeatTimer);
heartbeatTimer = null;
}
if (upstreamStallTimer) {
clearTimeout(upstreamStallTimer);
upstreamStallTimer = null;
}
req.off('close', closeUpstream);
try {
if (reader) {