Files
openchamber/packages/web/server/lib/opencode/startup-pipeline-runtime.js
T
Shyamalan KannanandBohdan Triapitsyn a9ee499ae4 fix: add concurrency controls for multiple sessions using the same provider (#1069)
* fix: add concurrency controls for multiple sessions using the same provider

Adds OS-inspired scheduling primitives (from HiveMind/AIMD research) to prevent
concurrent sessions from the same provider from experiencing slowdowns, random
stops, and cascading failures.

Server-side:
- Health check skips OpenCode restart when sessions are actively busy — a busy
  server under concurrent load can fail the health check timeout without being
  dead. Staleness guard forces restart if unhealthy+busy persists >2 minutes.
- Upstream SSE stall timeout scaled from 20s to 60s to avoid unnecessary
  reconnections when multiple sessions are waiting for LLM responses.

Client-side (HiveMind primitives, arXiv:2604.17111):
- Transparent retry with exponential backoff (1s→2s→4s, max 32s) for
  429/502/503/504 errors — the #1 most effective primitive from the paper.
- Circuit breaker: opens after 3 consecutive retryable errors, cooldown
  doubles each trip (30s→60s→120s, capped 128s), matching TCP AIMD.
- Per-provider session tracking with TTL eviction (1h idle sweep).
- Fetch-level retry gated on AbortError/TypeError only (not DNS failures).

Refs github-code-review skill findings (all 8 issues resolved).

* fix: use definite assignment assertion for response variable

Fixes TS2454: Variable 'response' is used before being assigned
in strict mode. The for-loop body always assigns it on every path
that reaches the post-loop code, but TS can't prove that.

* fix: add cleanupSession to error paths and remove unreachable code

P1 fixes (Greptile review):
- cleanupSession called on fetch error throw path
- cleanupSession called on non-retryable HTTP error throw path
- Removed unreachable post-loop code (loop always terminates via return or throw)

Adds explicit post-loop throw to satisfy TypeScript strict return check.

* fix: address Greptile review feedback on concurrent session controls

Removes client-side session tracking that leaked on normal completion paths.

The session tracking was redundant — the server-side health check already reads from

sessionRuntime.getSessionActivitySnapshot() for busy-session detection.

Changes:

- Remove activeSessions Set and all session-tracking functions from provider-tracker

- Remove trackSessionStarted/cleanupSession calls from client.ts

- Remove unreachable (response as Response) block after retry loop

- Make upstreamStallTimeoutMs conditional: 60s when >1 sessions, 20s otherwise

Refs #1069

* fix: enforce dynamic concurrency safeguards

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-01 14:57:31 +03:00

131 lines
3.2 KiB
JavaScript

export const createStartupPipelineRuntime = (dependencies) => {
const {
createTerminalRuntime,
createMessageStreamWsRuntime,
createServerStartupRuntime,
} = dependencies;
const run = async (options) => {
const {
app,
server,
express,
fs,
path,
uiAuthController,
buildAugmentedPath,
searchPathFor,
isExecutable,
isRequestOriginAllowed,
rejectWebSocketUpgrade,
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
globalEventHub,
processForwardedEventPayload,
messageStreamWsClients,
triggerHealthCheck,
upstreamStallTimeoutMs,
terminalHeartbeatIntervalMs,
terminalRebindWindowMs,
terminalMaxRebindsPerWindow,
setupProxy,
scheduleOpenCodeApiDetection,
bootstrapOpenCodeAtStartup,
staticRoutesRuntime,
process,
crypto,
normalizeTunnelBootstrapTtlMs,
readSettingsFromDiskMigrated,
tunnelAuthController,
startTunnelWithNormalizedRequest,
gracefulShutdown,
getSignalsAttached,
setSignalsAttached,
syncToHmrState,
TUNNEL_MODE_QUICK,
TUNNEL_MODE_MANAGED_LOCAL,
TUNNEL_MODE_MANAGED_REMOTE,
host,
port,
startupTunnelRequest,
onTunnelReady,
tunnelRuntimeContext,
attachSignals,
} = options;
const terminalRuntime = createTerminalRuntime({
app,
server,
express,
fs,
path,
uiAuthController,
buildAugmentedPath,
searchPathFor,
isExecutable,
isRequestOriginAllowed,
rejectWebSocketUpgrade,
TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS: terminalHeartbeatIntervalMs,
TERMINAL_INPUT_WS_REBIND_WINDOW_MS: terminalRebindWindowMs,
TERMINAL_INPUT_WS_MAX_REBINDS_PER_WINDOW: terminalMaxRebindsPerWindow,
});
const messageStreamRuntime = createMessageStreamWsRuntime({
server,
uiAuthController,
isRequestOriginAllowed,
rejectWebSocketUpgrade,
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
globalEventHub,
processForwardedEventPayload,
wsClients: messageStreamWsClients,
triggerHealthCheck,
upstreamStallTimeoutMs,
});
setupProxy(app);
scheduleOpenCodeApiDetection();
void bootstrapOpenCodeAtStartup();
staticRoutesRuntime.registerStaticRoutes(app);
const serverStartupRuntime = createServerStartupRuntime({
process,
crypto,
server,
normalizeTunnelBootstrapTtlMs,
readSettingsFromDiskMigrated,
tunnelAuthController,
startTunnelWithNormalizedRequest,
gracefulShutdown,
getSignalsAttached,
setSignalsAttached,
syncToHmrState,
TUNNEL_MODE_QUICK,
TUNNEL_MODE_MANAGED_LOCAL,
TUNNEL_MODE_MANAGED_REMOTE,
});
const bindHost = serverStartupRuntime.resolveBindHost(host);
const startupResult = await serverStartupRuntime.startListeningAndMaybeTunnel({
port,
bindHost,
startupTunnelRequest,
onTunnelReady,
});
tunnelRuntimeContext.setActivePort(startupResult.activePort);
serverStartupRuntime.attachProcessHandlers({ attachSignals });
return {
terminalRuntime,
messageStreamRuntime,
};
};
return {
run,
};
};