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
+16 -6
View File
@@ -40,6 +40,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
getManagedOpenCodeShellEnvSnapshot,
getManagedOpenCodeEnv = async () => ({}),
getActiveSessionCount = () => 0,
now = Date.now,
} = deps;
const killProcessOnPort = (port) => {
@@ -871,18 +872,21 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
const STALE_BUSY_GRACE_MS = 2 * 60 * 1000;
let lastUnhealthyWithBusySessionsAt = 0;
let consecutiveHealthFailures = 0;
let lastCountedHealthFailureAt = 0;
let healthProbePromise = null;
let healthCheckCyclePromise = null;
let lastHealthProbeResult = null;
let healthFailureCountIntervalMs = 15_000;
const resetHealthFailureState = () => {
consecutiveHealthFailures = 0;
lastUnhealthyWithBusySessionsAt = 0;
lastCountedHealthFailureAt = 0;
};
const probeOpenCodeHealth = async () => {
const now = Date.now();
if (lastHealthProbeResult && now - lastHealthProbeResult.at < HEALTH_CHECK_RESULT_CACHE_MS) {
const checkedAt = now();
if (lastHealthProbeResult && checkedAt - lastHealthProbeResult.at < HEALTH_CHECK_RESULT_CACHE_MS) {
return lastHealthProbeResult.healthy;
}
@@ -892,7 +896,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
healthProbePromise = isOpenCodeProcessHealthy()
.then((healthy) => {
lastHealthProbeResult = { at: Date.now(), healthy };
lastHealthProbeResult = { at: now(), healthy };
return healthy;
})
.finally(() => {
@@ -909,13 +913,13 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
return false;
}
const now = Date.now();
const checkedAt = now();
if (!lastUnhealthyWithBusySessionsAt) {
lastUnhealthyWithBusySessionsAt = now;
lastUnhealthyWithBusySessionsAt = checkedAt;
return true;
}
if (now - lastUnhealthyWithBusySessionsAt >= STALE_BUSY_GRACE_MS) {
if (checkedAt - lastUnhealthyWithBusySessionsAt >= STALE_BUSY_GRACE_MS) {
console.warn(
`[lifecycle] OpenCode unhealthy with ${activeCount} busy session(s) for > 2 min — forcing restart`
);
@@ -940,6 +944,11 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
await restartOpenCode();
return;
}
const checkedAt = now();
if (lastCountedHealthFailureAt && checkedAt - lastCountedHealthFailureAt < healthFailureCountIntervalMs) {
return;
}
lastCountedHealthFailureAt = checkedAt;
consecutiveHealthFailures += 1;
console.warn(
`[lifecycle] ${source} health check failed (${consecutiveHealthFailures}/${HEALTH_CHECK_MAX_CONSECUTIVE_FAILURES})`
@@ -974,6 +983,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
}
const effectiveIntervalMs = HEALTH_CHECK_INTERVAL_OVERRIDE_MS || healthCheckIntervalMs;
healthFailureCountIntervalMs = effectiveIntervalMs;
state.healthCheckInterval = setInterval(async () => {
try {